├── .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 |

>>> Improved version at PyTerminalMediaViewer <<<

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