This commit is contained in:
3
depends/rpi-rgb-led-matrix/utils/.gitignore
vendored
Normal file
3
depends/rpi-rgb-led-matrix/utils/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
led-image-viewer
|
||||
video-viewer
|
||||
text-scroller
|
||||
49
depends/rpi-rgb-led-matrix/utils/Makefile
Normal file
49
depends/rpi-rgb-led-matrix/utils/Makefile
Normal 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
|
||||
340
depends/rpi-rgb-led-matrix/utils/README.md
Normal file
340
depends/rpi-rgb-led-matrix/utils/README.md
Normal 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
|
||||
507
depends/rpi-rgb-led-matrix/utils/led-image-viewer.cc
Normal file
507
depends/rpi-rgb-led-matrix/utils/led-image-viewer.cc
Normal 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 ¶ms = 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;
|
||||
}
|
||||
324
depends/rpi-rgb-led-matrix/utils/text-scroller.cc
Normal file
324
depends/rpi-rgb-led-matrix/utils/text-scroller.cc
Normal 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;
|
||||
}
|
||||
467
depends/rpi-rgb-led-matrix/utils/video-viewer.cc
Normal file
467
depends/rpi-rgb-led-matrix/utils/video-viewer.cc
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user