├── .github
└── workflows
│ └── ccpp.yml
├── .gitignore
├── LICENSE
├── README.md
├── assets
├── iGif.gif
└── vGif.gif
├── makefile
└── src
├── formatList.h
├── include
├── dr_wav.h
├── miniaudio.h
└── stb_image.h
└── tmv.c
/.github/workflows/ccpp.yml:
--------------------------------------------------------------------------------
1 | name: make
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: install libs
13 | run:
14 | sudo apt update;
15 | sudo apt install -y ffmpeg;
16 | sudo apt install -y libavcodec-dev;
17 | sudo apt install -y libavformat-dev;
18 | sudo apt install -y libavfilter-dev;
19 | sudo apt install -y libavdevice-dev;
20 | - name: Build
21 | run: make;
22 |
23 | - name: Commit files
24 | run:
25 | rm tmv;
26 | rm .gitignore;
27 | rm -r .github;
28 | rm -r assets;
29 | git config --local user.email "githubActions";
30 | git config --local user.name "Auto Build Action";
31 | git add .;
32 | git commit -m "Build";
33 | - name: Push changes
34 | uses: ad-m/github-push-action@master
35 | with:
36 | github_token: ${{ secrets.GITHUB_TOKEN }}
37 | branch: release
38 | force: true
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /tmv
2 | /scan
3 | .vscode
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Kai Kitagawa-Jones
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
Terminal Media Viewer
3 | View images and videos without leaving the console
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ----
18 |
19 | ### Features
20 |
21 | * View **images** form any terminal
22 | * Watch **videos** from any terminal
23 | * Watch **youtube** videos from any terminal (`-y`, `--youtube`)
24 | * Play videos at **any fps** (`-f`, `--fps`, `-F`, `--origfps`)
25 | * **Resize** images / videos (`-w`, `-h`, `--width`, `--height`)
26 | * Easy to use
27 |
28 | ----
29 |
30 | ### Examples
31 |
32 | **Viewing an image**
33 |
34 |
35 |
36 | **Watching a video**
37 |
38 |
39 |
40 | ----
41 |
42 | ### Requirements
43 |
44 | * A terminal that supports **truecolor** ([list](https://gist.github.com/XVilka/8346728)) and **utf-8** (most terminals should support utf-8).
45 | * [ffmpeg](https://github.com/FFmpeg/FFmpeg) (only for videos)
46 | * [youtube-dl](https://github.com/ytdl-org/youtube-dl) (only for youtube videos)
47 |
48 | ----
49 |
50 | ### Usage
51 |
52 | **tmv [`OPTIONS...`] `[INPUT FILE / URL]`**
53 |
54 | * **`INPUT`**
55 | File to display/play
56 |
57 | * **`OPTIONS...`**
58 | * `-y`. `--youtube`
59 | View youtube videos
60 | * `-h`, `--height`
61 | Set height (setting both `width` and `height` will ignore original aspect ratio)
62 | * `-w`, `--width`
63 | Set width (setting both `width` and `height` will ignore original aspect ratio)
64 | * `-f`, `--fps`
65 | Set fps (default 15 fps)
66 | * `-F`, `--origfps`
67 | Use original fps from video (default 15 fps)
68 | * `-s`, `--no-sound`
69 | Disable sound
70 | * `-i`, `--no-info`
71 | Disable progress bar for videos
72 | * `-?`, `--help `
73 | Display help
74 | * `-V`
75 | Display version
76 |
77 | ----
78 |
79 | ### Installation
80 |
81 | #### Linux
82 |
83 | **Instructions:**
84 | 1. Install dependencies.
85 | * `libavcodec-dev`
86 | * `libavformat-dev`
87 | * `libavfilter-dev`
88 | * `libavdevice-dev`
89 |
90 | In addition, to watch videos install:
91 | * `ffmpeg`
92 | * `youtube-dl`
93 | 2. Clone the repository and run make.
94 | ```
95 | git clone https://github.com/kal39/TerminalMediaViewer.git
96 | cd TerminalMediaViewer
97 | make
98 | ```
99 | To install tmv to `/usr/local/bin` you can run `make install` (needs sudo privileges).
100 | To uninstall run `make uninstall` (needs sudo privileges).
101 |
102 | #### MacOS
103 |
104 | **Requirements:**
105 | * `homebrew`
106 | * `iterm2`
107 |
108 | **Instructions:**
109 | 1. Install dependencies
110 | ```
111 | brew install argp-standalone
112 | ```
113 | In addition, to watch videos:
114 | ```
115 | brew install ffmpeg
116 | brew install youtube-dl
117 | ```
118 | 2. Clone the repository and run make.
119 | ```
120 | git clone https://github.com/kal39/TerminalMediaViewer.git
121 | cd TerminalMediaViewer
122 | make
123 | ```
124 | To install tmv to `/usr/local/bin` you can run `make install` (needs sudo privileges).
125 | To uninstall run `make uninstall` (needs sudo privileges).
126 |
127 | > **Only works on iTerm2.**
128 |
129 | > There are some performance issues. Depending on the video encoding, your mileage may vary.
130 |
131 | ----
132 |
133 | ### Releases
134 |
135 | * **[`v0.1.1`](https://github.com/kal39/TerminalMediaViewer/releases/tag/v0.1.1) Youtube Support**
136 | TerminalMediaViewer can now play videos directly from youtube.
137 | To play videos from youtube, use the `-y` option.
138 |
139 | * Improved memory usage
140 | * Cursor is now hidden during videos
141 | * Supports spaces in video filenames
142 | * **Play videos directly from youtube**
143 | * Check if ffmpeg and YouTube exist before playing videos
144 | * Better error and debug messages
145 |
146 | * **[`v0.1`](https://github.com/kal39/TerminalMediaViewer/releases/tag/v0.1) Initial release**
147 | Initial release of tmv.
148 | It is in a very early state so bugs are expected.
149 |
150 | * View images
151 | * Watch videos (with sound)
152 | * Resize images / videos
153 |
154 | ----
155 |
156 | ### Contributing
157 | Any contributions are greatly appreciated.
158 |
159 | ----
160 |
161 | **kal39**(https://github.com/kal39) - kal390983@gmail.com
162 | Distributed under the MIT license. See `LICENSE` for more information.
163 |
--------------------------------------------------------------------------------
/assets/iGif.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kai-kj/TerminalMediaViewer/3255665c68ed3d77b5a5682f6fe659b08ff2b14f/assets/iGif.gif
--------------------------------------------------------------------------------
/assets/vGif.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kai-kj/TerminalMediaViewer/3255665c68ed3d77b5a5682f6fe659b08ff2b14f/assets/vGif.gif
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | #---- colors ------------------------------------------------------------------#
2 |
3 | BLACK := $(shell tput -Txterm setaf 0)
4 | RED := $(shell tput -Txterm setaf 1)
5 | GREEN := $(shell tput -Txterm setaf 2)
6 | YELLOW := $(shell tput -Txterm setaf 3)
7 | LIGHTPURPLE := $(shell tput -Txterm setaf 4)
8 | PURPLE := $(shell tput -Txterm setaf 5)
9 | BLUE := $(shell tput -Txterm setaf 6)
10 | WHITE := $(shell tput -Txterm setaf 7)
11 | RESET := $(shell tput -Txterm sgr0)
12 |
13 | #---- get info ----------------------------------------------------------------#
14 | OS = $(shell uname)
15 | SFLAG = $(shell id -u)
16 |
17 | ifeq ($(OS), )
18 | OS = Windows
19 | endif
20 |
21 | #---- set commands ------------------------------------------------------------#
22 |
23 | CC = gcc
24 |
25 | ifeq ($(OS), Windows)
26 | RM = del /f
27 | else
28 | RM = rm -f
29 | endif
30 |
31 | #---- set target and flags ----------------------------------------------------#
32 |
33 | TARGET = tmv
34 |
35 | FLAGS = -lm -lavcodec -lavformat -lavfilter -lavdevice -lswresample -lswscale -lavutil -lpthread -ldl
36 | OSXFLAGS = -lm -lavcodec -lavformat -lavfilter -lavdevice -lswresample -lswscale -lavutil -lpthread -ldl -largp
37 |
38 | #---- no debug flags ----------------------------------------------------------#
39 | release: clean
40 | @echo "Building..."
41 | @echo "OS: $(OS)"
42 | ifeq ($(OS), Darwin)
43 | # osx
44 | @echo "Building for osx..."
45 | @$(CC) -w src/$(TARGET).c $(OSXFLAGS) -o $(TARGET)
46 | else ifeq ($(OS), Windows)
47 | # windows
48 | @echo "Building for windows..."
49 | @$(CC) -w src\$(TARGET).c -o $(TARGET)
50 | else
51 | # linux
52 | @echo "Building for linux..."
53 | @$(CC) -w src/$(TARGET).c $(FLAGS) -o $(TARGET)
54 | endif
55 | @echo "$(GREEN)DONE$(RESET)"
56 |
57 | ifeq (, $(@shell which ffmpeg))
58 | @echo "$(YELLOW)NOTE$(RESET): ffmpeg is not installed (can't play videos)"
59 | endif
60 |
61 | ifeq (, $(@shell which youtube-dl))
62 | @echo "$(YELLOW)NOTE$(RESET): youtube-dl is not installed (can't download videos)"
63 | endif
64 |
65 | #---- enable debug flags ------------------------------------------------------#
66 | debug: clean
67 | @echo "Building with debug flag"
68 | @$(CC) -Wall src/$(TARGET).c $(FLAGS) -D DEBUG -o $(TARGET)
69 | @echo "$(GREEN)DONE$(RESET)"
70 |
71 | #---- make release and install to /usr/local/bin/ -----------------------------#
72 | install: uninstall release
73 | # check if sudo
74 | ifeq ($(SFLAG), 0)
75 | @echo "Installing..."
76 | @mv $(TARGET) /usr/local/bin
77 | @echo "$(GREEN)DONE$(RESET)"
78 | else
79 | @echo "$(RED)ERROR$(RESET): sudo privileges needed for install"
80 | endif
81 |
82 | #---- remove /usr/local/bin/tmv -----------------------------------------------#
83 | uninstall:
84 | # check if sudo
85 | ifeq ($(SFLAG), 0)
86 | @echo "Uninstalling..."
87 | @$(RM) /usr/local/bin/$(TARGET)
88 | @echo "$(GREEN)DONE$(RESET)"
89 | else
90 | @echo "$(RED)ERROR$(RESET): sudo privileges needed for uninstall"
91 | endif
92 |
93 | #---- for debuging with gdb ---------------------------------------------------#
94 | gdb: clean
95 | @$(CC) -g $(LIBS) src/$(TARGET).c $(FLAGS) -o $(TARGET).x
96 |
97 | #---- list deps ---------------------------------------------------------------#
98 | depend:
99 | @$(CC) $(LIBS) src/$(TARGET).c $(FLAGS) -M
100 |
101 | #---- remove local binaries ---------------------------------------------------#
102 | clean:
103 | @echo "Deleting old binaries..."
104 | @$(RM) $(TARGET)
105 | @$(RM) $(TARGET).x
106 | @echo "$(GREEN)DONE$(RESET)"
107 |
--------------------------------------------------------------------------------
/src/formatList.h:
--------------------------------------------------------------------------------
1 | char vFormats[][25] = {"3dostr", "3g2", "3gp", "4xm", "a64", "aa", "aac", "ac3", "acm", "act", "adf", "adp", "ads", "adts", "adx", "aea", "afc", "aiff", "aix", "alaw", "alias_pix", "alsa", "amr", "amrnb", "amrwb", "anm", "apc", "ape", "apng", "aptx", "aptx_hd", "aqtitle", "asf", "asf_o", "asf_stream", "ass", "ast", "au", "avi", "avm2", "avr", "avs", "avs2", "bethsoftvid", "bfi", "bfstm", "bin", "bink", "bit", "bmp_pipe", "bmv", "boa", "brender_pix", "brstm", "c93", "caf", "cavsvideo", "cdg", "cdxl", "cine", "codec2", "codec2raw", "concat", "crc", "dash", "data", "daud", "dcstr", "dds_pipe", "dfa", "dhav", "dirac", "dnxhd", "dpx_pipe", "dsf", "dsicin", "dss", "dts", "dtshd", "dv", "dvbsub", "dvbtxt", "dvd", "dxa", "ea", "ea_cdata", "eac3", "epaf", "exr_pipe", "f32be", "f32le", "f4v", "f64be", "f64le", "fbdev", "ffmetadata", "fifo", "fifo_test", "film_cpk", "filmstrip", "fits", "flac", "flic", "flv", "framecrc", "framehash", "framemd5", "frm", "fsb", "g722", "g723_1", "g726", "g726le", "g729", "gdv", "genh", "gif", "gif_pipe", "gsm", "gxf", "h261", "h263", "h264", "hash", "hcom", "hds", "hevc", "hls", "hnm", "ico", "idcin", "idf", "iec61883", "iff", "ifv", "ilbc", "image2", "image2pipe", "ingenient", "ipmovie", "ipod", "ircam", "ismv", "iss", "iv8", "ivf", "ivr", "j2k_pipe", "jack", "jacosub", "jpeg_pipe", "jpegls_pipe", "jv", "kmsgrab", "kux", "latm", "lavfi", "libmodplug", "live_flv", "lmlm4", "loas", "lrc", "lvf", "lxf", "m4v", "matroska", "matroska", "md5", "mgsts", "microdvd", "mjpeg", "mjpeg_2000", "mkv", "mkvtimestamp_v2", "mlp", "mlv", "mm", "mmf", "mov", "mov", "mp2", "mp3", "mp4", "mpc", "mpc8", "mpeg", "mpeg1video", "mpeg2video", "mpegts", "mpegtsraw", "mpegvideo", "mpjpeg", "mpl2", "mpsub", "msf", "msnwctcp", "mtaf", "mtv", "mulaw", "musx", "mv", "mvi", "mxf", "mxf_d10", "mxf_opatom", "mxg", "nc", "nistsphere", "nsp", "nsv", "null", "nut", "nuv", "oga", "ogg", "ogv", "oma", "opus", "oss", "paf", "pam_pipe", "pbm_pipe", "pcx_pipe", "pgm_pipe", "pgmyuv_pipe", "pictor_pipe", "pjs", "pmp", "png_pipe", "ppm_pipe", "psd_pipe", "psp", "psxstr", "pulse", "pva", "pvf", "qcp", "qdraw_pipe", "r3d", "rawvideo", "realtext", "redspark", "rl2", "rm", "roq", "rpl", "rsd", "rso", "rtp", "rtp_mpegts", "rtsp", "s16be", "s16le", "s24be", "s24le", "s32be", "s32le", "s337m", "s8", "sami", "sap", "sbc", "sbg", "scc", "sdl", "sdp", "sdr2", "sds", "sdx", "segment", "ser", "sgi_pipe", "shn", "siff", "singlejpeg", "sln", "smjpeg", "smk", "smoothstreaming", "smush", "sol", "sox", "spdif", "spx", "srt", "stl", "stream_segment", "subviewer", "subviewer1", "sunrast_pipe", "sup", "svag", "svcd", "svg_pipe", "swf", "tak", "tedcaptions", "tee", "thp", "tiertexseq", "tiff_pipe", "tmv", "truehd", "tta", "tty", "txd", "ty", "u16be", "u16le", "u24be", "u24le", "u32be", "u32le", "u8", "uncodedframecrc", "v210", "v210x", "vag", "vc1", "vc1test", "vcd", "vidc", "video4linux2", "vividas", "vivo", "vmd", "vob", "vobsub", "voc", "vpk", "vplayer", "vqf", "w64", "wav", "wc3movie", "webm", "webm_chunk", "webm_dash_manifest", "webp", "webp_pipe", "webvtt", "wsaud", "wsd", "wsvqa", "wtv", "wv", "wve", "x11grab", "xa", "xbin", "xmv", "xpm_pipe", "xv", "xvag", "xwd_pipe", "xwma", "yop", "yuv4mpegpip", "END"};
2 |
3 | char iFormats[][25] = {"JPG", "JPEG", "PNG", "TGA", "BMP", "PSD", "GIF", "HDR", "PIC", "PNM", "END"};
4 |
--------------------------------------------------------------------------------
/src/tmv.c:
--------------------------------------------------------------------------------
1 | //============================================================================//
2 | // Terminal Media Viewer
3 | //
4 | // View images and videos without leaving the console.
5 | // Requires a terminal that supports truecolor and utf-8
6 | //
7 | // https://github.com/kal39/TerminalMediaViewer
8 | //============================================================================//
9 |
10 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
11 | // Changelog
12 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
13 |
14 | v0.1.1 - Youtube Support
15 |
16 | * Improved memory usage
17 | * Cursor is now hidden during videos
18 | * Supports spaces in video filenames
19 | * Play videos directly from youtube
20 | * Check if ffmpeg and YouTube exist before playing videos
21 | * Better error and debug messages
22 |
23 | v0.1 - Initial release
24 |
25 | * View images
26 | * Watch videos (with sound)
27 | * Resize images / videos
28 |
29 | */
30 |
31 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
32 | // Usage
33 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
34 |
35 | tmv [OPTION...] [INPUT FILE / URL]
36 |
37 | -y, --youtube play video from youtube.
38 | -h, --height=[height] Set output height.
39 | -w, --width=[width] Set output width.
40 | -f, --fps=[target fps] Set target fps. Default 15 fps
41 | -F, --origfps Use original fps from video. Default 15 fps.
42 | -s, --no-sound disable sound.
43 | -?, --help Give this help list.
44 | --usage Give a short usage message.
45 | -V, --version Print program version.
46 |
47 | */
48 |
49 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
50 | // Licence
51 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
52 |
53 | /*
54 | MIT License
55 |
56 | Copyright (c) 2020 Kai Kitagawa-Jones
57 |
58 | Permission is hereby granted, free of charge, to any person obtaining a copy
59 | of this software and associated documentation files (the "Software"), to deal
60 | in the Software without restriction, including without limitation the rights
61 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
62 | copies of the Software, and to permit persons to whom the Software is
63 | furnished to do so, subject to the following conditions:
64 |
65 | The above copyright notice and this permission notice shall be included in all
66 | copies or substantial portions of the Software.
67 |
68 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
69 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
70 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
71 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
72 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
73 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
74 | SOFTWARE.
75 | */
76 |
77 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
78 | // Includes
79 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
80 |
81 | #define _POSIX_C_SOURCE 200809L
82 | #define _DEFAULT_SOURCE
83 |
84 | //-------- standard libraries ------------------------------------------------//
85 |
86 | #include
87 | #include
88 | #include
89 | #include
90 | #include
91 | #include
92 |
93 | //-------- POSIX libraries ---------------------------------------------------//
94 |
95 | #include
96 | #include
97 | #include
98 | #include
99 |
100 | //-------- ffmpeg ------------------------------------------------------------//
101 |
102 | #include
103 | #include
104 |
105 | //-------- external libraries ------------------------------------------------//
106 |
107 | // reading images
108 | #define STB_IMAGE_IMPLEMENTATION
109 | #include "include/stb_image.h"
110 |
111 | // playing audio files
112 | #define DR_WAV_IMPLEMENTATION
113 | #include "include/dr_wav.h"
114 |
115 | #define MINIAUDIO_IMPLEMENTATION
116 | #include "include/miniaudio.h"
117 |
118 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
119 | // Image / Video format lists
120 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
121 |
122 | #include "formatList.h"
123 | char vFormats[][25];
124 |
125 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
126 | // Defines
127 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
128 |
129 | #define DEFAULT_FPS 15
130 | #define TMP_FOLDER "/tmp/tmv"
131 |
132 | // number of samples to take when scaling (bigger -> better but slow)
133 | // 1 = nearest neighbor
134 | #define SCALE 5
135 |
136 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
137 | // Types
138 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
139 |
140 | typedef struct VideoInfo
141 | {
142 | int width;
143 | int height;
144 | int fps;
145 | float duration;
146 | }VideoInfo;
147 |
148 | typedef struct Pixel
149 | {
150 | unsigned short r;
151 | unsigned short g;
152 | unsigned short b;
153 | }Pixel;
154 |
155 | typedef struct Image
156 | {
157 | int width;
158 | int height;
159 | Pixel *pixels;
160 | }Image;
161 |
162 | void freeImage(Image *image)
163 | {
164 | if(image->pixels != NULL) free(image->pixels);
165 | image->pixels = NULL;
166 | }
167 |
168 | Image copyImage(Image image)
169 | {
170 | Image newImage;
171 | newImage.width = image.width;
172 | newImage.height = image.height;
173 |
174 | newImage.pixels = malloc((image.width * image.height) * sizeof(Pixel));
175 |
176 | for(int i = 0; i < image.height; i++)
177 | {
178 | for(int j = 0; j < image.width; j++)
179 | {
180 | int index = i * image.width + j;
181 | newImage.pixels[index].r = image.pixels[index].r;
182 | newImage.pixels[index].g = image.pixels[index].g;
183 | newImage.pixels[index].b = image.pixels[index].b;
184 | }
185 | }
186 | return(newImage);
187 | }
188 |
189 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
190 | // Debug
191 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
192 |
193 | int debugFunc(const char *FUNC, const char *FMT, ...)
194 | {
195 | #ifdef DEBUG
196 | char msg[4096];
197 | va_list args;
198 | va_start(args, FMT);
199 | vsnprintf(msg, sizeof(msg), FMT, args);
200 | printf(
201 | "\e[33mDEBUG\e[39m(\e[32m%s\e[39m) %s\n"
202 | "\e[90m[press ENTER to continue...]\e[39m",
203 | FUNC, msg
204 | );
205 | getchar();
206 | va_end(args);
207 | return 0;
208 | #else
209 | return 1;
210 | #endif
211 | }
212 |
213 | #define debug(a, ...) debugFunc(__func__, (a), ##__VA_ARGS__)
214 |
215 | void errorFunc(const char *FUNC, const char *FMT, ...)
216 | {
217 | char msg[4096];
218 | va_list args;
219 | va_start(args, FMT);
220 | vsnprintf(msg, sizeof(msg), FMT, args);
221 | printf("\e[31mERROR\e[39m(\e[32m%s\e[39m): %s\n", FUNC, msg);
222 | va_end(args);
223 | exit(1);
224 | }
225 |
226 | #define error(a, ...) errorFunc(__func__, (a), ##__VA_ARGS__)
227 |
228 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
229 | // Argp
230 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
231 |
232 | const char *argp_program_version = "tmv v0.1";
233 |
234 | const char *argp_program_bug_address = "";
235 |
236 | char doc[] =
237 | "\nView images and videos without leaving the console.\n"
238 | "Requires a terminal that supports truecolor and utf-8\n"
239 | "For more info visit ";
240 |
241 | char args_doc[] = "[INPUT FILE]";
242 |
243 | static struct argp_option options[] = {
244 | {"youtube", 'y', 0, 0, "play video from youtube\
245 | (use url for input file)", 1},
246 | {"width", 'w', "[width]", 0, "Set output width.", 2},
247 | {"height", 'h', "[height]", 0, "Set output height.", 2},
248 | {"fps", 'f', "[target fps]", 0, "Set target fps. Default 15 fps", 3},
249 | {"origfps", 'F', 0, 0, "Use original fps from video. Default 15 fps", 3},
250 | {"no-sound", 's', 0, 0, "disable sound", 3},
251 | {"no-info", 'i', 0, 0, "disable progress bar for videos", 3},
252 | { 0 }
253 | };
254 |
255 | struct args {
256 | char *input;
257 | int width;
258 | int height;
259 | int fps;
260 | int fpsFlag;
261 | int sound;
262 | int youtube;
263 | int bar;
264 | };
265 |
266 | static error_t parse_option(int key, char *arg, struct argp_state *state)
267 | {
268 | struct args *args = state->input;
269 | switch(key)
270 | {
271 | case ARGP_KEY_ARG:
272 | if(state->arg_num == 0)
273 | args->input = arg;
274 | else
275 | argp_usage( state );
276 | break;
277 | case 'f':
278 | if(atoi(arg) <= 0) error("invalid fps value");
279 | args->fps = atoi(arg);
280 | break;
281 | case 'F':
282 | args->fpsFlag = 1;
283 | break;
284 | case 'w':
285 | if(atoi(arg) <= 0) error("invalid width value");
286 | args->width = atoi(arg);
287 | break;
288 | case 'h':
289 | if(atoi(arg) <= 0) error("invalid height value");
290 | args->height = atoi(arg);
291 | break;
292 | case 's':
293 | args->sound = 0;
294 | break;
295 | case 'y':
296 | args->youtube = 1;
297 | break;
298 | case 'i':
299 | args->bar = 1;
300 | break;
301 | case ARGP_KEY_END:
302 | if(args->input == NULL)
303 | argp_usage( state );
304 | break;
305 | default:
306 | return ARGP_ERR_UNKNOWN;
307 | }
308 |
309 | return 0;
310 | }
311 |
312 | struct argp argp = {
313 | options,
314 | parse_option,
315 | args_doc,
316 | doc
317 | };
318 |
319 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
320 | // Misc
321 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
322 |
323 | float min(float a, float b)
324 | {
325 | if(a < b) return(a);
326 | else return(b);
327 | }
328 |
329 | int max(int a, int b)
330 | {
331 | if(a > b) return(a);
332 | else return(b);
333 | }
334 |
335 | float getTime()
336 | {
337 | struct timeval tv;
338 | gettimeofday(&tv, NULL);
339 | long int time = (long int)((tv.tv_sec) * 1000 + (tv.tv_usec) / 1000);
340 | return((float)(time % 10000000) / 1000);
341 | }
342 |
343 | // get file extension
344 | char *getExtension(const char *TARGET)
345 | {
346 | char *dot = strrchr(TARGET, '.');
347 | if(!dot || dot == TARGET) return "";
348 | else return dot + 1;
349 | }
350 |
351 | int checkFileType(const char EXT[])
352 | {
353 | int fileType = -1;
354 | for(int i = 0; ; i++)
355 | {
356 | if(strcasecmp(EXT, vFormats[i]) == 0)
357 | {
358 | fileType = 2;
359 | break;
360 | }
361 | else if(strcmp("END", vFormats[i]) == 0)
362 | break;
363 |
364 | }
365 |
366 | for(int i = 0; ; i++)
367 | {
368 | if(strcasecmp(EXT, iFormats[i]) == 0)
369 | {
370 | fileType = 1;
371 | break;
372 | }
373 | else if(strcmp("END", iFormats[i]) == 0)
374 | break;
375 |
376 | }
377 |
378 | return(fileType);
379 | }
380 |
381 | char *replaceWord(const char *STRING, const char *OLD, const char *NEW)
382 | {
383 | char *result;
384 | int i, cnt = 0;
385 | int newWlen = strlen(NEW);
386 | int oldWlen = strlen(OLD);
387 |
388 | // Counting the number of times old word
389 | // occur in the string
390 | for (i = 0; STRING[i] != '\0'; i++)
391 | {
392 | if (strstr(&STRING[i], OLD) == &STRING[i])
393 | {
394 | cnt++;
395 |
396 | // Jumping to index after the old word.
397 | i += oldWlen - 1;
398 | }
399 | }
400 |
401 | // Making new string of enough length
402 | result = (char *)malloc(i + cnt * (newWlen - oldWlen) + 1);
403 |
404 | i = 0;
405 | while (*STRING)
406 | {
407 | // compare the substring with the result
408 | if (strstr(STRING, OLD) == STRING)
409 | {
410 | strcpy(&result[i], NEW);
411 | i += newWlen;
412 | STRING += oldWlen;
413 | }
414 | else
415 | result[i++] = *STRING++;
416 | }
417 |
418 | result[i] = '\0';
419 | return result;
420 | }
421 |
422 | int getDigits(int input)
423 | {
424 | int count = 0;
425 | do
426 | {
427 | count++;
428 | input /= 10;
429 | } while(input != 0);
430 | return(count);
431 | }
432 |
433 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
434 | // Window
435 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
436 |
437 | int getWinWidth()
438 | {
439 | struct winsize size;
440 | ioctl(STDOUT_FILENO, TIOCGWINSZ, &size);
441 | return(size.ws_col);
442 | }
443 |
444 | int getWinHeight()
445 | {
446 | struct winsize size;
447 | ioctl(STDOUT_FILENO, TIOCGWINSZ, &size);
448 | return((size.ws_row) * 2);
449 | }
450 |
451 | // system("clear") doesn't let you scroll back
452 | void clear()
453 | {
454 | for(int i = 0; i < getWinHeight(); i++)
455 | printf("\n");
456 | }
457 |
458 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
459 | // Screen
460 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
461 |
462 | // only updates changed pixels
463 | void updateScreen(Image image, Image prevImage)
464 | {
465 | //Hide cursor (avoids that one white pixel when playing video)
466 | printf("\033[?25l");
467 |
468 | for(int i = 0; i < image.height - 1; i += 2) // update 2 pixels at once
469 | {
470 | for(int j = 0; j < image.width - 1; j++)
471 | {
472 | #define cPixel1 image.pixels[i * image.width + j]
473 | #define cPixel2 image.pixels[(i + 1) * image.width + j]
474 | #define pPixel1 prevImage.pixels[i * prevImage.width + j]
475 | #define pPixel2 prevImage.pixels[(i + 1) * prevImage.width + j]
476 |
477 | // draw only if pixel has changed
478 | if(
479 | cPixel1.r != pPixel1.r ||
480 | cPixel1.g != pPixel1.g ||
481 | cPixel1.b != pPixel1.b ||
482 | cPixel2.r != pPixel2.r ||
483 | cPixel2.g != pPixel2.g ||
484 | cPixel2.b != pPixel2.b
485 | )
486 | {
487 | // move cursor
488 | printf("\033[%d;%dH", i / 2 + 1, j + 1);
489 |
490 | // set foreground and background colors
491 | printf("\x1b[48;2;%d;%d;%dm", cPixel1.r, cPixel1.g, cPixel1.b);
492 | printf("\x1b[38;2;%d;%d;%dm", cPixel2.r, cPixel2.g, cPixel2.b);
493 |
494 | printf("▄");
495 | }
496 | }
497 | }
498 | }
499 |
500 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
501 | // Audio
502 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
503 |
504 | void data_callback(
505 | ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount
506 | )
507 | {
508 | ma_decoder* pDecoder = (ma_decoder*)pDevice->pUserData;
509 | if (pDecoder == NULL) {
510 | return;
511 | }
512 |
513 | ma_decoder_read_pcm_frames(pDecoder, pOutput, frameCount);
514 |
515 | (void)pInput;
516 | }
517 |
518 | ma_decoder decoder;
519 | ma_device device;
520 |
521 | void playAudio(const char PATH[])
522 | {
523 | ma_result result;
524 | ma_device_config deviceConfig;
525 |
526 | result = ma_decoder_init_file(PATH, NULL, &decoder);
527 |
528 | if(result != MA_SUCCESS)
529 | error("could not initialize decoder (use -s to disable audio)");
530 |
531 | deviceConfig = ma_device_config_init(ma_device_type_playback);
532 | deviceConfig.playback.format = decoder.outputFormat;
533 | deviceConfig.playback.channels = decoder.outputChannels;
534 | deviceConfig.sampleRate = decoder.outputSampleRate;
535 | deviceConfig.dataCallback = data_callback;
536 | deviceConfig.pUserData = &decoder;
537 |
538 | result = ma_device_init(NULL, &deviceConfig, &device);
539 |
540 | if(result != MA_SUCCESS)
541 | error("could not initialize device (use -s to disable audio)");
542 |
543 | result = ma_device_start(&device);
544 |
545 | if(result != MA_SUCCESS)
546 | error("could not start device (use -s to disable audio)");
547 | }
548 |
549 | void stopAudio()
550 | {
551 | ma_device_uninit(&device);
552 | ma_decoder_uninit(&decoder);
553 | }
554 |
555 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
556 | // Image
557 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
558 |
559 | int getImageWidth(const char TARGET[])
560 | {
561 | int width = -1;
562 | stbi_load(TARGET, &width, NULL, NULL, 3);
563 | return(width);
564 | }
565 |
566 | int getImageHeight(const char TARGET[])
567 | {
568 | int height = -1;
569 | stbi_load(TARGET, NULL, &height, NULL, 3);
570 | return(height);
571 | }
572 |
573 | Image loadImage(const char TARGET[])
574 | {
575 | Image image;
576 |
577 | unsigned char *imageRaw = stbi_load(
578 | TARGET, &image.width, &image.height, NULL, 3
579 | );
580 |
581 | if(imageRaw == NULL)
582 | error("could not open %s (it may be corrupt)", TARGET);
583 |
584 | image.pixels = (Pixel*)malloc((image.width * image.height) * sizeof(Pixel));
585 |
586 | if(image.pixels == NULL)
587 | error("failed to allocate memory for image");
588 |
589 | //debug("allocated mempry for image");
590 |
591 | // Convert to "Image" type (easier to use)
592 | for(int i = 0; i < image.height; i++)
593 | {
594 | for(int j = 0; j < image.width; j++)
595 | {
596 | image.pixels[i * image.width + j].r
597 | = imageRaw[i * image.width * 3 + j * 3];
598 | image.pixels[i * image.width + j].g
599 | = imageRaw[i * image.width * 3 + j * 3 + 1];
600 | image.pixels[i * image.width + j].b
601 | = imageRaw[i * image.width * 3 + j * 3 + 2];
602 | }
603 | }
604 |
605 | free(imageRaw);
606 | return(image);
607 | }
608 |
609 | Image scaleImage(Image oldImage, float zoomX, float zoomY)
610 | {
611 | Image newImage;
612 | newImage.width = (int)(oldImage.width * zoomX);
613 | newImage.height = (int)(oldImage.height * zoomY);
614 | float xPixelWidth = 1 / zoomX;
615 | float yPixelWidth = 1 / zoomY;
616 |
617 | newImage.pixels
618 | = (Pixel*)malloc((newImage.width * newImage.height) * sizeof(Pixel));
619 |
620 | if(newImage.pixels == NULL)
621 | error("failed to allocate memory for newImage");
622 |
623 | debug("allocated memory for newImage");
624 |
625 | for(int i = 0; i < newImage.height; i++)
626 | {
627 | for(int j = 0; j < newImage.width; j++)
628 | {
629 | #define pixel newImage.pixels[i * newImage.width + j]
630 | pixel.r = 0;
631 | pixel.g = 0;
632 | pixel.b = 0;
633 | int count = 0;
634 |
635 | // take the average of all points
636 | for(float k = 0; k < yPixelWidth; k += yPixelWidth / SCALE)
637 | {
638 | for(float l = 0; l < xPixelWidth; l += xPixelWidth / SCALE)
639 | {
640 | #define samplePoint oldImage.pixels\
641 | [(int)(floor(i * yPixelWidth + k) * oldImage.width)\
642 | + (int)floor(j * xPixelWidth + l)]
643 |
644 | pixel.r += samplePoint.r;
645 | pixel.g += samplePoint.g;
646 | pixel.b += samplePoint.b;
647 | count++;
648 | }
649 | }
650 |
651 | pixel.r = (int)((float)pixel.r / (float)count);
652 | pixel.g = (int)((float)pixel.g / (float)count);
653 | pixel.b = (int)((float)pixel.b / (float)count);
654 | }
655 | }
656 | return(newImage);
657 | }
658 |
659 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
660 | // Video
661 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
662 |
663 | void playVideo(const VideoInfo INFO, const int SOUND, const int BAR)
664 | {
665 | int height = getWinHeight();
666 |
667 | debug("tmp folder: %s", TMP_FOLDER);
668 |
669 | Image prevImage;
670 | prevImage.width = INFO.width;
671 | prevImage.height = INFO.height;
672 |
673 | prevImage.pixels
674 | = (Pixel*)malloc((INFO.width * INFO.height) * sizeof(Pixel));
675 |
676 | if(prevImage.pixels == NULL)
677 | error("failed to allocate memory for prevImage");
678 |
679 | debug("allocated memory for prevImage");
680 |
681 | // initialize all colors to -1 to force update when calling updateScreen()
682 | // for the first time
683 | for(int i = 0; i < INFO.height; i++)
684 | {
685 | for(int j = 0; j < INFO.width; j++)
686 | {
687 | prevImage.pixels[i * INFO.width + j].r = -1;
688 | prevImage.pixels[i * INFO.width + j].g = -1;
689 | prevImage.pixels[i * INFO.width + j].b = -1;
690 | }
691 | }
692 |
693 | char audioDir[1000];
694 | sprintf(audioDir, "%s/audio.wav", TMP_FOLDER);
695 |
696 | debug(
697 | "starting audio (%s) and video (%d fps)", audioDir, INFO.fps
698 | );
699 |
700 | float startTime = getTime();
701 |
702 | clear();
703 |
704 | if(SOUND == 1) playAudio(audioDir);
705 |
706 | while(1)
707 | {
708 | float time = getTime() - startTime;
709 | char file[1000];
710 | int currentFrame = (int)floor(INFO.fps * time);
711 | // frames start from 1
712 | if(currentFrame < 1)currentFrame = 1;
713 |
714 | sprintf(file, "%s/frame%d.bmp", TMP_FOLDER, currentFrame);
715 |
716 | if(access(file, F_OK) != - 1)
717 | {
718 | Image currentImage = loadImage(file);
719 | updateScreen(currentImage, prevImage);
720 | prevImage = copyImage(currentImage);
721 | freeImage(¤tImage);
722 | }
723 | else
724 | {
725 | error("next file (%s) not found", file);
726 | freeImage(&prevImage);
727 | break;
728 | }
729 |
730 | if(time > INFO.duration)
731 | {
732 | freeImage(&prevImage);
733 | break;
734 | }
735 |
736 | if(BAR == 0)
737 | {
738 | //move cursor to bottom left
739 | printf("\033[%d;%dH", height, 0);
740 |
741 | // reset colors
742 | printf("\e[40m\e[97m");
743 |
744 | //print time
745 | printf(
746 | "%02d:%02d / %02d:%02d ",
747 | (int)(time / 60),
748 | (int)time % 60,
749 | (int)(INFO.duration / 60),
750 | (int)INFO.duration % 60
751 | );
752 |
753 | int offset = max(2, getDigits((int)(time / 60)))
754 | + max(2, getDigits((int)(INFO.duration / 60))) + 11;
755 |
756 | int lineLength
757 | = (int)((float)(INFO.width - offset) * (time / INFO.duration));
758 |
759 | // print red bar
760 | printf("\e[31m");
761 | for(int i = 0; i < lineLength; i++)
762 | {
763 | printf("▬");
764 | }
765 |
766 | // print gray bar
767 | printf("\e[90m");
768 | for(int i = 0; i < INFO.width - offset - lineLength; i++)
769 | {
770 | printf("▬");
771 | }
772 | }
773 |
774 | }
775 | freeImage(&prevImage);
776 | }
777 |
778 | VideoInfo getVideoInfo(const char TARGET[])
779 | {
780 | av_register_all();
781 |
782 | VideoInfo info;
783 |
784 | AVFormatContext *formatCtx = avformat_alloc_context();
785 |
786 | if(formatCtx == NULL)
787 | error("failed to allocate memory for formatCtx");
788 |
789 | debug("allocated memory for formatCtx");
790 |
791 | if(avformat_open_input(&formatCtx, TARGET, NULL, NULL) < 0)
792 | error("failed to open file");
793 |
794 | avformat_find_stream_info(formatCtx, NULL);
795 |
796 | int index = 0;
797 |
798 | for (int i = 0; i < formatCtx->nb_streams; i++)
799 | {
800 | AVCodecParameters *codecPram = formatCtx->streams[i]->codecpar;
801 | if (codecPram->codec_type == AVMEDIA_TYPE_VIDEO) {
802 | info.width = codecPram->width;
803 | info.height = codecPram->height;
804 | index = i;
805 | break;
806 | }
807 | }
808 |
809 | info.fps = formatCtx->streams[index]->r_frame_rate.num;
810 | info.duration = formatCtx->duration / AV_TIME_BASE;
811 |
812 | debug(
813 | "got video info: %d * %d, fps = %d, time = %f[min]",
814 | info.width, info.height, info.fps,
815 | info.duration / 60
816 | );
817 |
818 | free(formatCtx);
819 |
820 | return(info);
821 | }
822 |
823 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
824 | // Cleanup
825 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
826 |
827 | void cleanup()
828 | {
829 | // move cursor to bottom right and reset colors and show cursor
830 | printf("\x1b[0m\033[?25h\033[%d;%dH\n", getWinWidth(), getWinHeight());
831 | char dirName[] = TMP_FOLDER;
832 |
833 | debug("tmp folder: %s", dirName);
834 |
835 | DIR *dir = opendir(dirName);
836 |
837 | if(dir == NULL)
838 | {
839 | debug("failed to open tmp folder");
840 | exit(0);
841 | }
842 |
843 | struct dirent *next_file;
844 | char filepath[NAME_MAX * 2 + 1];
845 |
846 | int count = 0;
847 |
848 | // delete all images that were left
849 | while((next_file = readdir(dir)) != NULL)
850 | {
851 | sprintf(filepath, "%s/%s", dirName, next_file->d_name);
852 | remove(filepath);
853 | count++;
854 | }
855 | closedir(dir);
856 |
857 | debug("deleted %d images", count);
858 |
859 | stopAudio();
860 |
861 | exit(0);
862 | }
863 |
864 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
865 | // Main
866 | //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
867 |
868 | //---- video -----------------------------------------------------------------//
869 |
870 | void video(
871 | const int WIDTH, const int HEIGHT,
872 | const int FPS, const int FLAG, const char INPUT[],
873 | const int SOUND, const int BAR
874 | )
875 | {
876 | debug("target: %s", INPUT);
877 |
878 | //check if ffmpeg is installed
879 | if(system("ffmpeg -h >>/dev/null 2>>/dev/null") != 0)
880 | error("ffmpeg is not installed");
881 |
882 | VideoInfo info = getVideoInfo(INPUT);
883 |
884 | float zoomX, zoomY;
885 | if(WIDTH == -1 && HEIGHT == -1)
886 | {
887 | zoomX = min(
888 | (float)getWinWidth() / (float)info.width,
889 | (float)getWinHeight() / (float)info.height
890 | );
891 | zoomY = zoomX;
892 | }
893 | else if(HEIGHT == -1)
894 | {
895 | zoomX = (float)WIDTH / (float)info.width;
896 | zoomY = zoomX;
897 | }
898 | else if(WIDTH == -1)
899 | {
900 | zoomY = (float)HEIGHT / (float)info.height;
901 | zoomX = zoomY;
902 | }
903 | else
904 | {
905 | zoomX = (float)WIDTH / (float)info.width;
906 | zoomY = (float)HEIGHT / (float)info.height;
907 | }
908 |
909 | debug("zoom: x: %f, y: %f", zoomX, zoomY);
910 |
911 | info.width *= zoomX;
912 | info.height *= zoomY;
913 |
914 | if(FLAG == 0)
915 | info.fps = DEFAULT_FPS;
916 |
917 | if(FPS != -1)
918 | info.fps = FPS;
919 |
920 | char dir[] = TMP_FOLDER;
921 |
922 | debug("tmp folder: %s", dir);
923 |
924 | struct stat sb;
925 |
926 | // make /tmp/tmv folder if none exists
927 | if(stat(dir, &sb) != 0)
928 | {
929 | debug("could not find tmp folder");
930 | mkdir(dir, 0700);
931 | debug("created tmp folder: %s", dir);
932 | }
933 |
934 | // decode video with ffmpeg into audio
935 | char commandA[1000];
936 | sprintf(
937 | commandA,
938 | "ffmpeg -i \"%s\" -f wav \"%s/audio.wav\" >>/dev/null 2>>/dev/null",
939 | INPUT, dir
940 | );
941 |
942 | debug("audio command: %s", commandA);
943 |
944 | // decode video with ffmpeg into bmp files
945 | char commandB[1000];
946 | sprintf(
947 | commandB,
948 | "ffmpeg -i \"%s\" -vf \"fps=%d, scale=%d:%d\" \"%s/frame%%d.bmp\"\
949 | >>/dev/null 2>>/dev/null",
950 | INPUT, info.fps, (int)(info.width), (int)(info.height),
951 | dir
952 | );
953 |
954 | debug("video command: %s", commandB);
955 |
956 | debug("forking");
957 |
958 | // child = plays video, parent = decodes
959 | int pid = fork();
960 |
961 | if(pid == 0)
962 | {
963 |
964 | char TARGET[1000];
965 | sprintf(TARGET, "%s/frame%d.bmp", dir, 1);
966 | // wait for first image (ffmpeg takes time to start)
967 | while(access(TARGET, F_OK) == -1){}
968 | // play the video
969 | playVideo(info, SOUND, BAR);
970 | }
971 | else
972 | {
973 | // audio first
974 | system(commandA);
975 | system(commandB);
976 | // wait for video to finish
977 | wait(NULL);
978 | }
979 | }
980 |
981 | //---- image -----------------------------------------------------------------//
982 |
983 | void image(const int WIDTH, const int HEIGHT, const char INPUT[])
984 | {
985 | debug("target image: %s", INPUT);
986 |
987 | Image image = loadImage(INPUT);
988 |
989 | debug(
990 | "original image dimensions: %d * %d",
991 | image.width, image.height
992 | );
993 |
994 | float zoomX, zoomY;
995 |
996 | if(WIDTH == -1 && HEIGHT == -1)
997 | {
998 | zoomX = min(
999 | (float)getWinWidth() / (float)image.width,
1000 | (float)getWinHeight() / (float)image.height
1001 | );
1002 | zoomY = zoomX;
1003 | }
1004 | else if(HEIGHT == -1)
1005 | {
1006 | zoomX = (float)WIDTH / (float)image.width;
1007 | zoomY = zoomX;
1008 | }
1009 | else if(WIDTH == -1)
1010 | {
1011 | zoomY = (float)HEIGHT / (float)image.height;
1012 | zoomX = zoomY;
1013 | }
1014 | else
1015 | {
1016 | zoomX = (float)WIDTH / (float)image.width;
1017 | zoomY = (float)HEIGHT / (float)image.height;
1018 | }
1019 |
1020 | debug("zoom: x: %f, y: %f", zoomX, zoomY);
1021 |
1022 | image = scaleImage(image, zoomX, zoomY);
1023 |
1024 | Image prevImage;
1025 | prevImage.width = image.width;
1026 | prevImage.height = image.height;
1027 |
1028 | prevImage.pixels
1029 | = (Pixel*)malloc((image.width * image.height) * sizeof(Pixel));
1030 |
1031 | if(prevImage.pixels == NULL)
1032 | error("failed to allocate memory for prevImage");
1033 |
1034 | debug("allocated memory for prevImage");
1035 |
1036 | // initialize all colors to -1 to force update when calling updateScreen()
1037 | for(int i = 0; i < image.height; i++)
1038 | {
1039 | for(int j = 0; j < image.width; j++)
1040 | {
1041 | prevImage.pixels[i * image.width + j].r = -1;
1042 | prevImage.pixels[i * image.width + j].g = -1;
1043 | prevImage.pixels[i * image.width + j].b = -1;
1044 | }
1045 | }
1046 |
1047 | clear();
1048 |
1049 | updateScreen(image, prevImage);
1050 |
1051 | freeImage(&image);
1052 | }
1053 |
1054 | //---- youtube ---------------------------------------------------------------//
1055 |
1056 | void youtube(
1057 | const int WIDTH, const int HEIGHT,
1058 | const int FPS, const int FLAG, const char INPUT[],
1059 | const int SOUND, const int BAR
1060 | )
1061 | {
1062 | //check if youtube-dl is installed
1063 | if(system("youtube-dl -h >>/dev/null 2>>/dev/null") != 0)
1064 | error("youtube-dl is not installed");
1065 |
1066 | struct stat sb;
1067 |
1068 | // make /tmp/tmv folder if none exists
1069 | if(stat(TMP_FOLDER, &sb) != 0)
1070 | {
1071 | debug("could not find tmp folder");
1072 | mkdir(TMP_FOLDER, 0700);
1073 | debug("created tmp folder: %s", TMP_FOLDER);
1074 | }
1075 |
1076 | // download video with youtube-dl
1077 | char command[1000];
1078 | sprintf(
1079 | command,
1080 | "youtube-dl --geo-bypass --ignore-config -q --no-warnings -f mp4 \
1081 | -o \"%s/video.%%(ext)s\" %s >>/dev/null 2>>/dev/null",
1082 | TMP_FOLDER, INPUT
1083 | );
1084 |
1085 | debug("command: %s", command);
1086 |
1087 | debug("downloading video");
1088 |
1089 | if(system(command) != 0)
1090 | error("could not download video");
1091 |
1092 | char dir[1000];
1093 |
1094 | sprintf(dir, "%s/video.mp4", TMP_FOLDER);
1095 |
1096 | // wait for first image (ffmpeg takes time to start)
1097 | while(access(dir, F_OK) == -1){}
1098 |
1099 | debug("finished downloading video");
1100 |
1101 | video(WIDTH, HEIGHT, FPS, FLAG, dir, SOUND, BAR);
1102 | }
1103 |
1104 | //---- main ------------------------------------------------------------------//
1105 |
1106 | int main(int argc, char *argv[])
1107 | {
1108 | // cleanup on ctr+c
1109 | signal(SIGINT, cleanup);
1110 |
1111 | // setup argp
1112 | struct args args = {0};
1113 |
1114 | // default values
1115 | args.fps = -1;
1116 | args.fpsFlag = 0;
1117 | args.width = -1;
1118 | args.height = -1;
1119 | args.sound = 1;
1120 | args.youtube = 0;
1121 | args.bar = 0;
1122 |
1123 | argp_parse(&argp, argc, argv, 0, 0, &args);
1124 |
1125 | if(args.youtube == 1)
1126 | {
1127 | debug("youtube mode");
1128 | youtube(
1129 | args.width, args.height, args.fps,
1130 | args.fpsFlag, args.input,
1131 | args.sound, args.bar
1132 | );
1133 | }
1134 | else
1135 | {
1136 | debug("target file: %s", args.input);
1137 |
1138 | if(access(args.input, F_OK) == -1)
1139 | error("%s does not exist. If it is a url use the -y flag", args.input);
1140 |
1141 | char *ext = getExtension(args.input);
1142 |
1143 | debug("file extension: %s", ext);
1144 |
1145 | int fileType = checkFileType(ext);
1146 |
1147 | if(fileType == 1)
1148 | image(args.width, args.height, args.input);
1149 | else if(fileType == 2)
1150 | video(
1151 | args.width, args.height, args.fps,
1152 | args.fpsFlag, args.input,
1153 | args.sound, args.bar
1154 | );
1155 | else
1156 | error("invalid file type");
1157 | }
1158 |
1159 | cleanup();
1160 |
1161 | return(0);
1162 | }
1163 |
--------------------------------------------------------------------------------