removing submodule
Some checks failed
build lightwatch / build (push) Failing after 5m6s

This commit is contained in:
2023-09-03 01:14:34 -04:00
parent d539dc9119
commit ed18dd911a
190 changed files with 1174526 additions and 4 deletions

View File

@@ -0,0 +1,3 @@
led-image-viewer
video-viewer
text-scroller

View File

@@ -0,0 +1,49 @@
CXXFLAGS=-O3 -W -Wall -Wextra -Wno-unused-parameter -D_FILE_OFFSET_BITS=64
OBJECTS=led-image-viewer.o text-scroller.o
BINARIES=led-image-viewer text-scroller
OPTIONAL_OBJECTS=video-viewer.o
OPTIONAL_BINARIES=video-viewer
# Where our library resides. You mostly only need to change the
# RGB_LIB_DISTRIBUTION, this is where the library is checked out.
RGB_LIB_DISTRIBUTION=..
RGB_INCDIR=$(RGB_LIB_DISTRIBUTION)/include
RGB_LIBDIR=$(RGB_LIB_DISTRIBUTION)/lib
RGB_LIBRARY_NAME=rgbmatrix
RGB_LIBRARY=$(RGB_LIBDIR)/lib$(RGB_LIBRARY_NAME).a
RGB_LDFLAGS+=-L$(RGB_LIBDIR) -l$(RGB_LIBRARY_NAME) -lrt -lm -lpthread
# Imagemagic flags, only needed if actually compiled.
MAGICK_CXXFLAGS?=$(shell GraphicsMagick++-config --cppflags --cxxflags)
MAGICK_LDFLAGS?=$(shell GraphicsMagick++-config --ldflags --libs)
AV_CXXFLAGS=$(shell pkg-config --cflags libavcodec libavformat libswscale libavutil)
AV_LDFLAGS=$(shell pkg-config --cflags --libs libavcodec libavformat libswscale libavutil)
simple: $(BINARIES)
all : $(BINARIES) $(OPTIONAL_BINARIES)
$(RGB_LIBRARY): FORCE
$(MAKE) -C $(RGB_LIBDIR)
text-scroller: text-scroller.o $(RGB_LIBRARY)
$(CXX) $(CXXFLAGS) text-scroller.o -o $@ $(LDFLAGS) $(RGB_LDFLAGS)
led-image-viewer: led-image-viewer.o $(RGB_LIBRARY)
$(CXX) $(CXXFLAGS) led-image-viewer.o -o $@ $(LDFLAGS) $(RGB_LDFLAGS) $(MAGICK_LDFLAGS)
video-viewer: video-viewer.o $(RGB_LIBRARY)
$(CXX) $(CXXFLAGS) video-viewer.o -o $@ $(LDFLAGS) $(RGB_LDFLAGS) $(AV_LDFLAGS)
%.o : %.cc
$(CXX) -I$(RGB_INCDIR) $(CXXFLAGS) -c -o $@ $<
led-image-viewer.o : led-image-viewer.cc
$(CXX) -I$(RGB_INCDIR) $(CXXFLAGS) $(MAGICK_CXXFLAGS) -c -o $@ $<
clean:
rm -f $(OBJECTS) $(BINARIES) $(OPTIONAL_OBJECTS) $(OPTIONAL_BINARIES)
FORCE:
.PHONY: FORCE

View File

@@ -0,0 +1,340 @@
Utilities
=========
This contains useful utilities that might be directly useful without having
to write any code.
Below, the description of the utilities contains a list of commandline flags
they support. Next to the specific flags for their use, they all
have a set of standard options that always come with the LED matrix,
such as choosing the `--led-rows` or `--led-chain`.
For brevity, we don't repeat them below in the synopsis prints of each of
these utilities. You find a description of the standard options in
the [toplevel readme](../README.md#changing-parameters-via-command-line-flags)
<details><summary>Unfold: Standard LED-matrix options present in all utilities</summary>
```
--led-gpio-mapping=<name> : Name of GPIO mapping used. Default "regular"
--led-rows=<rows> : Panel rows. Typically 8, 16, 32 or 64. (Default: 32).
--led-cols=<cols> : Panel columns. Typically 32 or 64. (Default: 32).
--led-chain=<chained> : Number of daisy-chained panels. (Default: 1).
--led-parallel=<parallel> : Parallel chains. range=1..3 (Default: 1).
--led-multiplexing=<0..11> : Mux type: 0=direct; 1=Stripe; 2=Checkered; 3=Spiral; 4=ZStripe; 5=ZnMirrorZStripe; 6=coreman; 7=Kaler2Scan; 8=ZStripeUneven; 9=P10-128x4-Z; 10=QiangLiQ8; 11=InversedZStripe (Default: 0)
--led-pixel-mapper : Semicolon-separated list of pixel-mappers to arrange pixels.
Optional params after a colon e.g. "U-mapper;Rotate:90"
Available: "Mirror", "Rotate", "U-mapper". Default: ""
--led-pwm-bits=<1..11> : PWM bits (Default: 11).
--led-brightness=<percent>: Brightness in percent (Default: 100).
--led-scan-mode=<0..1> : 0 = progressive; 1 = interlaced (Default: 0).
--led-row-addr-type=<0..4>: 0 = default; 1 = AB-addressed panels; 2 = direct row select; 3 = ABC-addressed panels; 4 = ABC Shift + DE direct (Default: 0).
--led-show-refresh : Show refresh rate.
--led-inverse : Switch if your matrix has inverse colors on.
--led-rgb-sequence : Switch if your matrix has led colors swapped (Default: "RGB")
--led-pwm-lsb-nanoseconds : PWM Nanoseconds for LSB (Default: 130)
--led-pwm-dither-bits=<0..2> : Time dithering of lower bits (Default: 0)
--led-no-hardware-pulse : Don't use hardware pin-pulse generation.
--led-panel-type=<name> : Needed to initialize special panels. Supported: 'FM6126A'
--led-slowdown-gpio=<0..4>: Slowdown GPIO. Needed for faster Pis/slower panels (Default: 1).
--led-daemon : Make the process run in the background as daemon.
--led-no-drop-privs : Don't drop privileges from 'root' after initializing the hardware.
--led-drop-priv-user : Drop privileges to this username or UID (Default: 'daemon')
--led-drop-priv-group : Drop privileges to this groupname or GID (Default: 'daemon')
```
</details>
### Image Viewer ###
The image viewer reads all kinds of image formats, including animated gifs.
To speed up lengthy loading of image files or animations, you also can also
pre-process images or animations and write them to a 'stream' file that then
later can be loaded very quickly by this viewer (at the expense of disk-space
as these are not compressed). This is in particular useful for large panels
and animations with many frames: less loading time and less RAM used.
See `-O` example below in the example section.
##### Building
The `led-image-viewer` requires the GraphicsMagick dependency first, then
it can be built with `make led-image-viewer`.
```
sudo apt-get update
sudo apt-get install libgraphicsmagick++-dev libwebp-dev -y
make led-image-viewer
```
##### Usage
The resulting binary has a couple of flags.
```
usage: ./led-image-viewer [options] <image> [option] [<image> ...]
Options:
-O<streamfile> : Output to stream-file instead of matrix (Don't need to be root).
-C : Center images.
These options affect images FOLLOWING them on the command line,
so it is possible to have different options for each image
-w<seconds> : Regular image: Wait time in seconds before next image is shown (default: 1.5).
-t<seconds> : For animations: stop after this time.
-l<loop-count> : For animations: number of loops through a full cycle.
-D<animation-delay-ms> : For animations: override the delay between frames given in the
gif/stream animation with this value. Use -1 to use default value.
-V<vsync-multiple> : For animation (expert): Only do frame vsync-swaps on multiples of refresh (default: 1)
(Tip: use --led-limit-refresh for stable rate)
Options affecting display of multiple images:
-f : Forever cycle through the list of files on the command line.
-s : If multiple images are given: shuffle.
General LED matrix options:
<... all the --led- options>
Switch time between files: -w for static images; -t/-l for animations
Animated gifs: If both -l and -t are given, whatever finishes first determines duration.
The -w, -t and -l options apply to the following images until a new instance of one of these options is seen.
So you can choose different durations for different images.
```
Then, you can run it with any common image format, including animated gifs:
##### Examples
```bash
sudo ./led-image-viewer some-image.jpg # Display an image.
sudo ./led-image-viewer animated.gif # Show an animated gif
sudo ./led-image-viewer -t5 animated.gif # Show an animated gif for 5 seconds
sudo ./led-image-viewer -l2 animated.gif # Show an animated gif for 2 loops
sudo ./led-image-viewer -D16 animated.gif # Play animated gif, use 16ms frame delay
# If you want to have an even frame rate, that is depending on your
# refresh rate, use the following. Note, your refresh rate is dependent on
# factors such as chain length and rows; use --led-show-refresh to get an idea.
# Then fix it with --led-limit-refresh
sudo ./led-image-viewer --led-limit-refresh=200 -D0 -V10 animated.gif # Frame rate = 1/12 refresh rate
sudo ./led-image-viewer -w3 foo.jpg bar.png # show two images, wait 3 seconds between. Stop.
sudo ./led-image-viewer -w3 foo.jpg -w2 bar.png baz.png # show images, wait 3 seconds after the first, 2 seconds after the second and third. Stop.
sudo ./led-image-viewer -f -w3 foo.jpg bar.png # show images, wait 3sec between, go back and loop forever
sudo ./led-image-viewer -f -w3 *.png *.jpg # Loop forever through a list of images
sudo ./led-image-viewer -f -s *.png # Loop forever but randomize (shuffle) each round.
# Show image.png and animated.gif in a loop. Show the static image for 3 seconds
# while the animation is shown for 5 seconds (-t takes precedence for animated
# images over -w)
sudo ./led-image-viewer -f -w3 -t5 image.png animated.gif
# Create a fast animation from a bunch of *.png files
# with 16.6ms frame time (=60Hz) and write to a raw animation stream
# animation-out.stream (beware, uncompressed, uses lots of disk).
# Note:
# o We have to supply all the options (rows, chain, parallel, hardware-mapping,
# rotation etc), that we would supply to the real viewer later.
# o We don't need to be root, as we don't write to the matrix
./led-image-viewer --led-rows=32 --led-chain=4 --led-parallel=3 -w0.016667 *.png -Oanimation-out.stream
# Now, play back this animation.
sudo ./led-image-viewer --led-rows=32 --led-chain=4 --led-parallel=3 animation-out.stream
```
### Text Scroller ###
The text scroller allows to show some scrolling text.
##### Building
```
make text-scroller
```
##### Usage
```
usage: ./text-scroller [options] <text>
Takes text and scrolls it with speed -s
Options:
-f <font-file> : Path to *.bdf-font to be used.
-i <textfile> : Input from file.
-s <speed> : Approximate letters per second.
Positive: scroll right to left; Negative: scroll left to right
(Zero for no scrolling)
-l <loop-count> : Number of loops through the text. -1 for endless (default)
-b <on-time>,<off-time> : Blink while scrolling. Keep on and off for these amount of scrolled pixels.
-x <x-origin> : Shift X-Origin of displaying text (Default: 0)
-y <y-origin> : Shift Y-Origin of displaying text (Default: 0)
-t <track-spacing>: Spacing pixels between letters (Default: 0)
-C <r,g,b> : Text Color. Default 255,255,255 (white)
-B <r,g,b> : Background-Color. Default 0,0,0
-O <r,g,b> : Outline-Color, e.g. to increase contrast.
General LED matrix options:
<... all the --led- options>
```
You need to specify a font for the tool to use. We are using BDF-fonts,
which are bitmap fonts nicely suited for low-resolution displays such as ours.
A few fonts you find in the
[../fonts](../fonts) directory. The [README.md](../fonts/README.md) there also describes
how to make your own.
The program directly takes the text found on the command line and scrolls
it over the screen.
Alternatively, with the `-i` option, a file is read with the text to be
scrolled. The file is watched, and if the content changes, the `text-scroller`
automatically updates the scroll text.
##### Examples
```bash
# (use your --led-rows, --led-chain and --led-parallel suited for your setup)
# Print simple 'Hello world'
sudo ./text-scroller -f ../fonts/9x18.bdf "Hello World ♥"
# Read input from text file (will pick up changes when file changes)
echo "Hello world" > input.txt
sudo ./text-scroller -f ../fonts/9x18.bdf -i input.txt
# Red (-C) text on a display with 4 chained displays. Notice you can use UTF-8 characters
# if they are supported by the font.
sudo ./text-scroller -f ../fonts/9x18.bdf -C255,0,0 --led-chain=4 "Hello World ♥"
# .. faster speed; roughly 20 characters per second with option -s.
sudo ./text-scroller -f ../fonts/9x18.bdf -C255,0,0 --led-chain=4 -s20 "The quick brown fox jumps over the lazy dog"
# A speed of zero does just shows the text, no scrolling.
sudo ./text-scroller -f ../fonts/9x18.bdf -C255,0,0 --led-chain=4 -s0 "No Scroll"
# A text might need to be arranged a bit. Let's move it 15 pixels to the right and 5 down:
sudo ./text-scroller -f ../fonts/9x18.bdf -C255,0,0 --led-chain=4 -s0 -x15 -y5 "Shifted"
# Now text in red color on a blue background (-B). We choose an outline (-O)
# of a slightly darker blue for better contrast
sudo ./text-scroller -f ../fonts/9x18.bdf -B0,0,255 -O0,0,100 -C255,0,0 --led-chain=4 "Contrast outline"
# A larger font. This one needs a bit of an y-adjustment
# (move up 11 pixels: a negative y shift) to fit nicely on a panel.
sudo ./text-scroller -f ../fonts/texgyre-27.bdf --led-chain=4 -y-11 "Large Font"
```
### Video Viewer ###
The video viewer allows to play common video formats on the RGB matrix (just
the picture, no sound).
This is currently doing a software decode; if you are familiar with the
av libraries, a pull request that adds hardware decoding is welcome.
Right now, this is CPU intensive and decoding can result in an output that
is not smooth or presents flicker, in particular on older Pis.
If you observe that, it is suggested to
prepare a preprocessed stream that you then later watch with `led-image-viewer`
(see example below). This will use a bit of disk-space, but it will result
in best quality as all the expensive calculation has been done beforehand.
Short of that, if you want to use the video viewer directly (e.g. because the
stream file would be super-large), do the following when you observe flicker:
- Use the `-T` option to add more decode threads; `-T2` or `-T3` typically.
- Transcode the video first to the width and height of the final output size
so that decoding and scaling is much cheaper at runtime.
- If you use tools such as [youtube-dl] to acquire the video, tell it
to choose a low resolution version (e.g. for that program use option
`-f"[height<480]"`).
- Synchronize output as integer fraction of matrix refresh rate (example
below).
- Another route to watch videos is to run a [flaschen-taschen]
server on your Pi, that provides a network interface to your LED-Matrix.
Now, you can use [vlc] from some other computer on your network and
stream the output to your Pi.
You have to provide the IP address and size of the panel:
```
vlc --vout flaschen --flaschen-display=<IP-address-of-your-pi> \
--flaschen-width=128 --flaschen-height=64 \
<video-filename-or-YouTube-URL>
```
##### Building
The video-viewer requires some dependencies first, then it can be
built with `make video-viewer`.
```
sudo apt-get update
sudo apt-get install pkg-config libavcodec-dev libavformat-dev libswscale-dev
make video-viewer
```
##### Usage
```
Show one or a sequence of video files on the RGB-Matrix
usage: ./video-viewer [options] <video> [<video>...]
Options:
-F : Full screen without black bars; aspect ratio might suffer
-O<streamfile> : Output to stream-file instead of matrix (don't need to be root).
-s <count> : Skip these number of frames in the beginning.
-c <count> : Only show this number of frames (excluding skipped frames).
-V<vsync-multiple> : Instead of native video framerate, playback framerate
is a fraction of matrix refresh. In particular with a stable refresh,
this can result in more smooth playback. Choose multiple for desired framerate.
(Tip: use --led-limit-refresh for stable rate)
-T <threads> : Number of threads used to decode (default 1, max=4)
-v : verbose; prints video metadata and other info.
-f : Loop forever.
General LED matrix options:
<... all the --led- options>
```
##### Examples
```bash
# Play video. If you observe that the Pi has trouble to keep up (extensive
# flickering), transcode the video first to the exact size of your display.
sudo ./video-viewer --led-chain=4 --led-parallel=3 -T2 myvideo.webm
# If you observe flicker you can try to synchronize video output with
# the refresh rate of the panel. For that, first figure out with
# --led-show-refresh what the 'natural' refresh rate is of your LED panel.
# Then choose one that is lower and a multiple of the frame-rate of the
# video. Let's say we have a video with a framerate of 25fps and find that
# our panel can refresh with more than 200Hz (after the usual refresh
# tweakings such as with --led-pwm-dither-bits).
# Let's fix the refresh rate to 200 and sync a new frame with every
# 8th refresh to get the desired video fps (200/8 = 25)
sudo ./video-viewer --led-chain=4 --led-parallel=3 --led-limit-refresh=200 -V8 myvideo.webm
```
**Example preparing a preprocessed stream**
```bash
# A way to avoid flicker playback with best possible results even with
# very high framerate: create a preprocessed stream first, then replay it with
# led-image-viewer. This results in best quality (no CPU use at play-time), but
# comes with a caveat: It can use _A LOT_ of disk, as it is not compressed.
# Note:
# o We don't need to be root, as we don't write to the matrix, just to a file.
# o We have to supply all the options (rows, chain, parallel, hardware-mapping,
# rotation etc), that we would supply to the real viewer later as the
# framebuffer is fully pre-processed to avoid any overhead while playing.
# o You could even run this on your much faster regular Linux PC (little
# endian) and create a stream that you then can play on your Pi.
# ----- STEP 1 Preprocessing ------
./video-viewer --led-chain=5 --led-parallel=3 -T4 myvideo.mp4 -O/tmp/vid.stream
#.. now play the resulting stream with the with led-image-viewer. Also try
# using -D or -V to replay with different frame rate.
# Note, you need to give the same options (rows, chain, parallel etc.) as
# you did when creating the stream.
# ----- STEP 2 Actual Playing ------
sudo ./led-image-viewer --led-chain=5 --led-parallel=3 /tmp/vid.stream
```
[youtube-dl]: https://youtube-dl.org/
[flaschen-taschen]: https://github.com/hzeller/flaschen-taschen/tree/master/server#rgb-matrix-panel-display
[vlc]: https://www.videolan.org/vlc

View File

@@ -0,0 +1,507 @@
// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
// Copyright (C) 2015 Henner Zeller <h.zeller@acm.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation version 2.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://gnu.org/licenses/gpl-2.0.txt>
// To use this image viewer, first get image-magick development files
// $ sudo apt-get install libgraphicsmagick++-dev libwebp-dev
//
// Then compile with
// $ make led-image-viewer
#include "led-matrix.h"
#include "pixel-mapper.h"
#include "content-streamer.h"
#include <fcntl.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <algorithm>
#include <map>
#include <string>
#include <vector>
#include <Magick++.h>
#include <magick/image.h>
using rgb_matrix::Canvas;
using rgb_matrix::FrameCanvas;
using rgb_matrix::RGBMatrix;
using rgb_matrix::StreamReader;
typedef int64_t tmillis_t;
static const tmillis_t distant_future = (1LL<<40); // that is a while.
struct ImageParams {
ImageParams() : anim_duration_ms(distant_future), wait_ms(1500),
anim_delay_ms(-1), loops(-1), vsync_multiple(1) {}
tmillis_t anim_duration_ms; // If this is an animation, duration to show.
tmillis_t wait_ms; // Regular image: duration to show.
tmillis_t anim_delay_ms; // Animation delay override.
int loops;
int vsync_multiple;
};
struct FileInfo {
ImageParams params; // Each file might have specific timing settings
bool is_multi_frame;
rgb_matrix::StreamIO *content_stream;
};
volatile bool interrupt_received = false;
static void InterruptHandler(int signo) {
interrupt_received = true;
}
static tmillis_t GetTimeInMillis() {
struct timeval tp;
gettimeofday(&tp, NULL);
return tp.tv_sec * 1000 + tp.tv_usec / 1000;
}
static void SleepMillis(tmillis_t milli_seconds) {
if (milli_seconds <= 0) return;
struct timespec ts;
ts.tv_sec = milli_seconds / 1000;
ts.tv_nsec = (milli_seconds % 1000) * 1000000;
nanosleep(&ts, NULL);
}
static void StoreInStream(const Magick::Image &img, int delay_time_us,
bool do_center,
rgb_matrix::FrameCanvas *scratch,
rgb_matrix::StreamWriter *output) {
scratch->Clear();
const int x_offset = do_center ? (scratch->width() - img.columns()) / 2 : 0;
const int y_offset = do_center ? (scratch->height() - img.rows()) / 2 : 0;
for (size_t y = 0; y < img.rows(); ++y) {
for (size_t x = 0; x < img.columns(); ++x) {
const Magick::Color &c = img.pixelColor(x, y);
if (c.alphaQuantum() < 255) {
scratch->SetPixel(x + x_offset, y + y_offset,
ScaleQuantumToChar(c.redQuantum()),
ScaleQuantumToChar(c.greenQuantum()),
ScaleQuantumToChar(c.blueQuantum()));
}
}
}
output->Stream(*scratch, delay_time_us);
}
static void CopyStream(rgb_matrix::StreamReader *r,
rgb_matrix::StreamWriter *w,
rgb_matrix::FrameCanvas *scratch) {
uint32_t delay_us;
while (r->GetNext(scratch, &delay_us)) {
w->Stream(*scratch, delay_us);
}
}
// Load still image or animation.
// Scale, so that it fits in "width" and "height" and store in "result".
static bool LoadImageAndScale(const char *filename,
int target_width, int target_height,
bool fill_width, bool fill_height,
std::vector<Magick::Image> *result,
std::string *err_msg) {
std::vector<Magick::Image> frames;
try {
readImages(&frames, filename);
} catch (std::exception& e) {
if (e.what()) *err_msg = e.what();
return false;
}
if (frames.size() == 0) {
fprintf(stderr, "No image found.");
return false;
}
// Put together the animation from single frames. GIFs can have nasty
// disposal modes, but they are handled nicely by coalesceImages()
if (frames.size() > 1) {
Magick::coalesceImages(result, frames.begin(), frames.end());
} else {
result->push_back(frames[0]); // just a single still image.
}
const int img_width = (*result)[0].columns();
const int img_height = (*result)[0].rows();
const float width_fraction = (float)target_width / img_width;
const float height_fraction = (float)target_height / img_height;
if (fill_width && fill_height) {
// Scrolling diagonally. Fill as much as we can get in available space.
// Largest scale fraction determines that.
const float larger_fraction = (width_fraction > height_fraction)
? width_fraction
: height_fraction;
target_width = (int) roundf(larger_fraction * img_width);
target_height = (int) roundf(larger_fraction * img_height);
}
else if (fill_height) {
// Horizontal scrolling: Make things fit in vertical space.
// While the height constraint stays the same, we can expand to full
// width as we scroll along that axis.
target_width = (int) roundf(height_fraction * img_width);
}
else if (fill_width) {
// dito, vertical. Make things fit in horizontal space.
target_height = (int) roundf(width_fraction * img_height);
}
for (size_t i = 0; i < result->size(); ++i) {
(*result)[i].scale(Magick::Geometry(target_width, target_height));
}
return true;
}
void DisplayAnimation(const FileInfo *file,
RGBMatrix *matrix, FrameCanvas *offscreen_canvas) {
const tmillis_t duration_ms = (file->is_multi_frame
? file->params.anim_duration_ms
: file->params.wait_ms);
rgb_matrix::StreamReader reader(file->content_stream);
int loops = file->params.loops;
const tmillis_t end_time_ms = GetTimeInMillis() + duration_ms;
const tmillis_t override_anim_delay = file->params.anim_delay_ms;
for (int k = 0;
(loops < 0 || k < loops)
&& !interrupt_received
&& GetTimeInMillis() < end_time_ms;
++k) {
uint32_t delay_us = 0;
while (!interrupt_received && GetTimeInMillis() <= end_time_ms
&& reader.GetNext(offscreen_canvas, &delay_us)) {
const tmillis_t anim_delay_ms =
override_anim_delay >= 0 ? override_anim_delay : delay_us / 1000;
const tmillis_t start_wait_ms = GetTimeInMillis();
offscreen_canvas = matrix->SwapOnVSync(offscreen_canvas,
file->params.vsync_multiple);
const tmillis_t time_already_spent = GetTimeInMillis() - start_wait_ms;
SleepMillis(anim_delay_ms - time_already_spent);
}
reader.Rewind();
}
}
static int usage(const char *progname) {
fprintf(stderr, "usage: %s [options] <image> [option] [<image> ...]\n",
progname);
fprintf(stderr, "Options:\n"
"\t-O<streamfile> : Output to stream-file instead of matrix (Don't need to be root).\n"
"\t-C : Center images.\n"
"\nThese options affect images FOLLOWING them on the command line,\n"
"so it is possible to have different options for each image\n"
"\t-w<seconds> : Regular image: "
"Wait time in seconds before next image is shown (default: 1.5).\n"
"\t-t<seconds> : "
"For animations: stop after this time.\n"
"\t-l<loop-count> : "
"For animations: number of loops through a full cycle.\n"
"\t-D<animation-delay-ms> : "
"For animations: override the delay between frames given in the\n"
"\t gif/stream animation with this value. Use -1 to use default value.\n"
"\t-V<vsync-multiple> : For animation (expert): Only do frame vsync-swaps on multiples of refresh (default: 1)\n"
"\t (Tip: use --led-limit-refresh for stable rate)\n"
"\nOptions affecting display of multiple images:\n"
"\t-f : "
"Forever cycle through the list of files on the command line.\n"
"\t-s : If multiple images are given: shuffle.\n"
);
fprintf(stderr, "\nGeneral LED matrix options:\n");
rgb_matrix::PrintMatrixFlags(stderr);
fprintf(stderr,
"\nSwitch time between files: "
"-w for static images; -t/-l for animations\n"
"Animated gifs: If both -l and -t are given, "
"whatever finishes first determines duration.\n");
fprintf(stderr, "\nThe -w, -t and -l options apply to the following images "
"until a new instance of one of these options is seen.\n"
"So you can choose different durations for different images.\n");
return 1;
}
int main(int argc, char *argv[]) {
Magick::InitializeMagick(*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]);
}
bool do_forever = false;
bool do_center = false;
bool do_shuffle = false;
// We remember ImageParams for each image, which will change whenever
// there is a flag modifying them. This map keeps track of filenames
// and their image params (also for unrelated elements of argv[], but doesn't
// matter).
// We map the pointer instad of the string of the argv parameter so that
// we can have two times the same image on the commandline list with different
// parameters.
std::map<const void *, struct ImageParams> filename_params;
// Set defaults.
ImageParams img_param;
for (int i = 0; i < argc; ++i) {
filename_params[argv[i]] = img_param;
}
const char *stream_output = NULL;
int opt;
while ((opt = getopt(argc, argv, "w:t:l:fr:c:P:LhCR:sO:V:D:")) != -1) {
switch (opt) {
case 'w':
img_param.wait_ms = roundf(atof(optarg) * 1000.0f);
break;
case 't':
img_param.anim_duration_ms = roundf(atof(optarg) * 1000.0f);
break;
case 'l':
img_param.loops = atoi(optarg);
break;
case 'D':
img_param.anim_delay_ms = atoi(optarg);
break;
case 'f':
do_forever = true;
break;
case 'C':
do_center = true;
break;
case 's':
do_shuffle = true;
break;
case 'r':
fprintf(stderr, "Instead of deprecated -r, use --led-rows=%s instead.\n",
optarg);
matrix_options.rows = atoi(optarg);
break;
case 'c':
fprintf(stderr, "Instead of deprecated -c, use --led-chain=%s instead.\n",
optarg);
matrix_options.chain_length = atoi(optarg);
break;
case 'P':
matrix_options.parallel = atoi(optarg);
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 'O':
stream_output = strdup(optarg);
break;
case 'V':
img_param.vsync_multiple = atoi(optarg);
if (img_param.vsync_multiple < 1) img_param.vsync_multiple = 1;
break;
case 'h':
default:
return usage(argv[0]);
}
// Starting from the current file, set all the remaining files to
// the latest change.
for (int i = optind; i < argc; ++i) {
filename_params[argv[i]] = img_param;
}
}
const int filename_count = argc - optind;
if (filename_count == 0) {
fprintf(stderr, "Expected image filename.\n");
return usage(argv[0]);
}
// Prepare matrix
runtime_opt.do_gpio_init = (stream_output == NULL);
RGBMatrix *matrix = RGBMatrix::CreateFromOptions(matrix_options, runtime_opt);
if (matrix == NULL)
return 1;
FrameCanvas *offscreen_canvas = matrix->CreateFrameCanvas();
printf("Size: %dx%d. Hardware gpio mapping: %s\n",
matrix->width(), matrix->height(), matrix_options.hardware_mapping);
// These parameters are needed once we do scrolling.
const bool fill_width = false;
const bool fill_height = false;
// In case the output to stream is requested, set up the stream object.
rgb_matrix::StreamIO *stream_io = NULL;
rgb_matrix::StreamWriter *global_stream_writer = NULL;
if (stream_output) {
int fd = open(stream_output, O_CREAT|O_WRONLY, 0644);
if (fd < 0) {
perror("Couldn't open output stream");
return 1;
}
stream_io = new rgb_matrix::FileStreamIO(fd);
global_stream_writer = new rgb_matrix::StreamWriter(stream_io);
}
const tmillis_t start_load = GetTimeInMillis();
fprintf(stderr, "Loading %d files...\n", argc - optind);
// Preparing all the images beforehand as the Pi might be too slow to
// be quickly switching between these. So preprocess.
std::vector<FileInfo*> file_imgs;
for (int imgarg = optind; imgarg < argc; ++imgarg) {
const char *filename = argv[imgarg];
FileInfo *file_info = NULL;
std::string err_msg;
std::vector<Magick::Image> image_sequence;
if (LoadImageAndScale(filename, matrix->width(), matrix->height(),
fill_width, fill_height, &image_sequence, &err_msg)) {
file_info = new FileInfo();
file_info->params = filename_params[filename];
file_info->content_stream = new rgb_matrix::MemStreamIO();
file_info->is_multi_frame = image_sequence.size() > 1;
rgb_matrix::StreamWriter out(file_info->content_stream);
for (size_t i = 0; i < image_sequence.size(); ++i) {
const Magick::Image &img = image_sequence[i];
int64_t delay_time_us;
if (file_info->is_multi_frame) {
delay_time_us = img.animationDelay() * 10000; // unit in 1/100s
} else {
delay_time_us = file_info->params.wait_ms * 1000; // single image.
}
if (delay_time_us <= 0) delay_time_us = 100 * 1000; // 1/10sec
StoreInStream(img, delay_time_us, do_center, offscreen_canvas,
global_stream_writer ? global_stream_writer : &out);
}
} else {
// Ok, not an image. Let's see if it is one of our streams.
int fd = open(filename, O_RDONLY);
if (fd >= 0) {
file_info = new FileInfo();
file_info->params = filename_params[filename];
file_info->content_stream = new rgb_matrix::FileStreamIO(fd);
StreamReader reader(file_info->content_stream);
if (reader.GetNext(offscreen_canvas, NULL)) { // header+size ok
file_info->is_multi_frame = reader.GetNext(offscreen_canvas, NULL);
reader.Rewind();
if (global_stream_writer) {
CopyStream(&reader, global_stream_writer, offscreen_canvas);
}
} else {
err_msg += "; Can't read as image or compatible stream";
delete file_info->content_stream;
delete file_info;
file_info = NULL;
}
}
else {
perror("Opening file");
}
}
if (file_info) {
file_imgs.push_back(file_info);
} else {
fprintf(stderr, "%s skipped: Unable to open (%s)\n",
filename, err_msg.c_str());
}
}
if (stream_output) {
delete global_stream_writer;
delete stream_io;
if (file_imgs.size()) {
fprintf(stderr, "Done: Output to stream %s; "
"this can now be opened with led-image-viewer with the exact same panel configuration settings such as rows, chain, parallel and hardware-mapping\n", stream_output);
}
if (do_shuffle)
fprintf(stderr, "Note: -s (shuffle) does not have an effect when generating streams.\n");
if (do_forever)
fprintf(stderr, "Note: -f (forever) does not have an effect when generating streams.\n");
// Done, no actual output to matrix.
return 0;
}
// Some parameter sanity adjustments.
if (file_imgs.empty()) {
// e.g. if all files could not be interpreted as image.
fprintf(stderr, "No image could be loaded.\n");
return 1;
} else if (file_imgs.size() == 1) {
// Single image: show forever.
file_imgs[0]->params.wait_ms = distant_future;
} else {
for (size_t i = 0; i < file_imgs.size(); ++i) {
ImageParams &params = file_imgs[i]->params;
// Forever animation ? Set to loop only once, otherwise that animation
// would just run forever, stopping all the images after it.
if (params.loops < 0 && params.anim_duration_ms == distant_future) {
params.loops = 1;
}
}
}
fprintf(stderr, "Loading took %.3fs; now: Display.\n",
(GetTimeInMillis() - start_load) / 1000.0);
signal(SIGTERM, InterruptHandler);
signal(SIGINT, InterruptHandler);
do {
if (do_shuffle) {
std::random_shuffle(file_imgs.begin(), file_imgs.end());
}
for (size_t i = 0; i < file_imgs.size() && !interrupt_received; ++i) {
DisplayAnimation(file_imgs[i], matrix, offscreen_canvas);
}
} while (do_forever && !interrupt_received);
if (interrupt_received) {
fprintf(stderr, "Caught signal. Exiting.\n");
}
// Animation finished. Shut down the RGB matrix.
matrix->Clear();
delete matrix;
// Leaking the FileInfos, but don't care at program end.
return 0;
}

View File

@@ -0,0 +1,324 @@
// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
// Copyright (C) 2015 Henner Zeller <h.zeller@acm.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation version 2.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://gnu.org/licenses/gpl-2.0.txt>
#include "led-matrix.h"
#include "graphics.h"
#include <algorithm>
#include <fstream>
#include <streambuf>
#include <string>
#include <getopt.h>
#include <math.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
using namespace rgb_matrix;
volatile bool interrupt_received = false;
static void InterruptHandler(int signo) {
interrupt_received = true;
}
static int usage(const char *progname) {
fprintf(stderr, "usage: %s [options] [<text>| -i <filename>]\n", progname);
fprintf(stderr, "Takes text and scrolls it with speed -s\n");
fprintf(stderr, "Options:\n");
fprintf(stderr,
"\t-f <font-file> : Path to *.bdf-font to be used.\n"
"\t-i <textfile> : Input from file.\n"
"\t-s <speed> : Approximate letters per second. \n"
"\t Positive: scroll right to left; Negative: scroll left to right\n"
"\t (Zero for no scrolling)\n"
"\t-l <loop-count> : Number of loops through the text. "
"-1 for endless (default)\n"
"\t-b <on-time>,<off-time> : Blink while scrolling. Keep "
"on and off for these amount of scrolled pixels.\n"
"\t-x <x-origin> : Shift X-Origin of displaying text (Default: 0)\n"
"\t-y <y-origin> : Shift Y-Origin of displaying text (Default: 0)\n"
"\t-t <track-spacing>: Spacing pixels between letters (Default: 0)\n"
"\n"
"\t-C <r,g,b> : Text Color. Default 255,255,255 (white)\n"
"\t-B <r,g,b> : Background-Color. Default 0,0,0\n"
"\t-O <r,g,b> : Outline-Color, e.g. to increase contrast.\n"
);
fprintf(stderr, "\nGeneral LED matrix options:\n");
rgb_matrix::PrintMatrixFlags(stderr);
return 1;
}
static bool parseColor(Color *c, const char *str) {
return sscanf(str, "%hhu,%hhu,%hhu", &c->r, &c->g, &c->b) == 3;
}
static bool FullSaturation(const Color &c) {
return (c.r == 0 || c.r == 255)
&& (c.g == 0 || c.g == 255)
&& (c.b == 0 || c.b == 255);
}
static void add_micros(struct timespec *accumulator, long micros) {
const long billion = 1000000000;
const int64_t nanos = (int64_t) micros * 1000;
accumulator->tv_sec += nanos / billion;
accumulator->tv_nsec += nanos % billion;
while (accumulator->tv_nsec > billion) {
accumulator->tv_nsec -= billion;
accumulator->tv_sec += 1;
}
}
// Read line and return if it changed.
typedef uint64_t stat_fingerprint_t;
static bool ReadLineOnChange(const char *filename, std::string *out,
stat_fingerprint_t *last_file_status) {
struct stat sb;
if (stat(filename, &sb) < 0) {
perror("Couldn't determine file change");
return false;
}
const stat_fingerprint_t fp = ((uint64_t)sb.st_mtime << 32) + sb.st_size;
if (fp == *last_file_status) {
return false; // no change according to stat()
}
*last_file_status = fp;
std::ifstream fs(filename);
std::string str((std::istreambuf_iterator<char>(fs)),
std::istreambuf_iterator<char>());
std::replace(str.begin(), str.end(), '\n', ' ');
if (*out == str) {
return false; // no content change
}
*out = str;
return true;
}
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]);
}
Color color(255, 255, 255);
Color bg_color(0, 0, 0);
Color outline_color(0,0,0);
bool with_outline = false;
const char *bdf_font_file = NULL;
const char *input_file = NULL;
std::string line;
bool xorigin_configured = false;
int x_orig = 0;
int y_orig = 0;
int letter_spacing = 0;
float speed = 7.0f;
int loops = -1;
int blink_on = 0;
int blink_off = 0;
int opt;
while ((opt = getopt(argc, argv, "x:y:f:C:B:O:t:s:l:b:i:")) != -1) {
switch (opt) {
case 's': speed = atof(optarg); break;
case 'b':
if (sscanf(optarg, "%d,%d", &blink_on, &blink_off) == 1) {
blink_off = blink_on;
}
fprintf(stderr, "hz: on=%d off=%d\n", blink_on, blink_off);
break;
case 'l': loops = atoi(optarg); break;
case 'x': x_orig = atoi(optarg); xorigin_configured = true; break;
case 'y': y_orig = atoi(optarg); break;
case 'f': bdf_font_file = strdup(optarg); break;
case 'i': input_file = strdup(optarg); break;
case 't': letter_spacing = atoi(optarg); break;
case 'C':
if (!parseColor(&color, optarg)) {
fprintf(stderr, "Invalid color spec: %s\n", optarg);
return usage(argv[0]);
}
break;
case 'B':
if (!parseColor(&bg_color, optarg)) {
fprintf(stderr, "Invalid background color spec: %s\n", optarg);
return usage(argv[0]);
}
break;
case 'O':
if (!parseColor(&outline_color, optarg)) {
fprintf(stderr, "Invalid outline color spec: %s\n", optarg);
return usage(argv[0]);
}
with_outline = true;
break;
default:
return usage(argv[0]);
}
}
stat_fingerprint_t last_change = 0;
if (input_file) {
if (!ReadLineOnChange(input_file, &line, &last_change)) {
fprintf(stderr, "Couldn't read file '%s'\n", input_file);
return usage(argv[0]);
}
}
else {
for (int i = optind; i < argc; ++i) {
line.append(argv[i]).append(" ");
}
if (line.empty()) {
fprintf(stderr, "Add the text you want to print on the command-line or -i for input file.\n");
return usage(argv[0]);
}
}
if (bdf_font_file == NULL) {
fprintf(stderr, "Need to specify BDF font-file with -f\n");
return usage(argv[0]);
}
/*
* Load font. This needs to be a filename with a bdf bitmap font.
*/
rgb_matrix::Font font;
if (!font.LoadFont(bdf_font_file)) {
fprintf(stderr, "Couldn't load font '%s'\n", bdf_font_file);
return 1;
}
/*
* If we want an outline around the font, we create a new font with
* the original font as a template that is just an outline font.
*/
rgb_matrix::Font *outline_font = NULL;
if (with_outline) {
outline_font = font.CreateOutlineFont();
}
RGBMatrix *canvas = RGBMatrix::CreateFromOptions(matrix_options, runtime_opt);
if (canvas == NULL)
return 1;
const bool all_extreme_colors = (matrix_options.brightness == 100)
&& FullSaturation(color)
&& FullSaturation(bg_color)
&& FullSaturation(outline_color);
if (all_extreme_colors)
canvas->SetPWMBits(1);
signal(SIGTERM, InterruptHandler);
signal(SIGINT, InterruptHandler);
printf("CTRL-C for exit.\n");
// Create a new canvas to be used with led_matrix_swap_on_vsync
FrameCanvas *offscreen_canvas = canvas->CreateFrameCanvas();
const int scroll_direction = (speed >= 0) ? -1 : 1;
speed = fabs(speed);
int delay_speed_usec = 1000000;
if (speed > 0) {
delay_speed_usec = 1000000 / speed / font.CharacterWidth('W');
}
if (!xorigin_configured) {
if (speed == 0) {
// There would be no scrolling, so text would never appear. Move to front.
x_orig = with_outline ? 1 : 0;
} else {
x_orig = scroll_direction < 0 ? canvas->width() : 0;
}
}
int x = x_orig;
int y = y_orig;
int length = 0;
struct timespec next_frame = {0, 0};
uint64_t frame_counter = 0;
while (!interrupt_received && loops != 0) {
if (input_file && ReadLineOnChange(input_file, &line, &last_change)) {
x = x_orig;
}
++frame_counter;
offscreen_canvas->Fill(bg_color.r, bg_color.g, bg_color.b);
const bool draw_on_frame = (blink_on <= 0)
|| (frame_counter % (blink_on + blink_off) < (uint64_t)blink_on);
if (draw_on_frame) {
if (outline_font) {
// The outline font, we need to write with a negative (-2) text-spacing,
// as we want to have the same letter pitch as the regular text that
// we then write on top.
rgb_matrix::DrawText(offscreen_canvas, *outline_font,
x - 1, y + font.baseline(),
outline_color, NULL,
line.c_str(), letter_spacing - 2);
}
// length = holds how many pixels our text takes up
length = rgb_matrix::DrawText(offscreen_canvas, font,
x, y + font.baseline(),
color, NULL,
line.c_str(), letter_spacing);
}
x += scroll_direction;
if ((scroll_direction < 0 && x + length < 0) ||
(scroll_direction > 0 && x > canvas->width())) {
x = x_orig + ((scroll_direction > 0) ? -length : 0);
if (loops > 0) --loops;
}
// Make sure render-time delays are not influencing scroll-time
if (speed > 0) {
if (next_frame.tv_sec == 0 && next_frame.tv_nsec == 0) {
// First time. Start timer, but don't wait.
clock_gettime(CLOCK_MONOTONIC, &next_frame);
} else {
add_micros(&next_frame, delay_speed_usec);
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_frame, NULL);
}
}
// Swap the offscreen_canvas with canvas on vsync, avoids flickering
offscreen_canvas = canvas->SwapOnVSync(offscreen_canvas);
if (speed <= 0) pause(); // Nothing to scroll.
}
// Finished. Shut down the RGB matrix.
canvas->Clear();
delete canvas;
return 0;
}

View File

@@ -0,0 +1,467 @@
// -*- 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;
}