diff options
author | James Houghton | 2017-06-29 13:11:05 -0400 |
---|---|---|
committer | James Houghton | 2017-06-29 13:11:05 -0400 |
commit | 765865a7d019c500f293a7a653e7cc54aa832a75 (patch) | |
tree | 096fc8443e390ab404bdc926129d51d616582f97 | |
download | aur-765865a7d019c500f293a7a653e7cc54aa832a75.tar.gz |
Add 0.0.1 ttyvideo
-rw-r--r-- | .SRCINFO | 16 | ||||
-rw-r--r-- | .gitignore | 10 | ||||
-rw-r--r-- | CMakeLists.txt | 21 | ||||
-rw-r--r-- | PKGBUILD | 21 | ||||
-rw-r--r-- | README.md | 21 | ||||
-rw-r--r-- | handle.c | 174 | ||||
-rw-r--r-- | handle.h | 30 | ||||
-rw-r--r-- | ttyvideo.cpp | 306 |
8 files changed, 599 insertions, 0 deletions
diff --git a/.SRCINFO b/.SRCINFO new file mode 100644 index 000000000000..4e71dd43c3c8 --- /dev/null +++ b/.SRCINFO @@ -0,0 +1,16 @@ +pkgbase = ttyvideo + pkgdesc = ttyvideo displays videos in the terminal. + pkgver = 0.0.1 + pkgrel = 1 + url = https://github.com/jamesthoughton/ttyvideo + arch = any + license = MIT + depends = opencv, + depends = gsteamer, + depends = gst-plugins-base, + depends = cmake + source = https://github.com/jamesthoughton/ttyvideo/archive/0.0.1.tar.gz + md5sums = 862c8cf756ad896f82654565ffc7f5ad + +pkgname = ttyvideo + diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000000..0c7f348b2bfc --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +opencv/ +CMake* +!CMakeLists.txt +cmake_install.cmake +*.a +*.o +*.so +ttyvideo +Makefile +install_manifest.txt diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000000..2cc034700bdd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 2.8) +project(ttyvideo) +find_package(OpenCV REQUIRED) + +if(MSVC) + # Visual Studio -- /W4 + if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]") + string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") + endif() +elseif(CMAKE_COMPILER_IS_GNUXX OR CMAKE_COMPILER_IS_GNUCC) + # GCC -- -Wall -pedantic + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic") +endif() + +add_library(handle STATIC handle.c) +add_executable(ttyvideo ttyvideo.cpp) +target_link_libraries(ttyvideo ${OpenCV_LIBS} -lm handle) + +install(TARGETS ttyvideo DESTINATION bin) diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 000000000000..102c5468e152 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,21 @@ +pkgname=ttyvideo +pkgver='0.0.1' +pkgrel='1' +pkgdesc="ttyvideo displays videos in the terminal." +arch=('any') +license=('MIT') +depends=('opencv', 'gsteamer', 'gst-plugins-base', 'cmake') +source=("https://github.com/jamesthoughton/ttyvideo/archive/0.0.1.tar.gz") +url='https://github.com/jamesthoughton/ttyvideo' +md5sums=('862c8cf756ad896f82654565ffc7f5ad') + +build() { + cd $srcdir/ttyvideo + cmake . + make +} + +package() { + cd $srcdir/ttyvideo + install -Dm 755 ttyvideo $pkgdir/usr/bin/ttyvideo +} diff --git a/README.md b/README.md new file mode 100644 index 000000000000..671d6f9be322 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# ttyvideo +Pushing the limits of resolution and color depth... backwards! +ttyvideo is a video player that runs in a 256-color terminal. +## Dependencies +ttyvideo requires OpenCV (2 or 3) compiled with video codecs. If your package manager doesn't carry a version of OpenCV like this, you're going to need to download and compile its [source code](https://github.com/opencv/opencv "OpenCV source") before you can use ttyvideo. +ttyvideo also uses CMake>=2.8 to generate files used in the installation process. +## Installation +You can compile ttyvideo with CMake and Make. +```shell +cmake . +make +``` +After compiling, it can be installed by performing +```shell +sudo make install +``` + +## How it works +ttyvideo works with most visual media types: videos, images, GIFs, etc. This is due to the usage of OpenCV to read media. +Right now, ttyvideo doesn't do anything special when rendering videos. +ttyvideo will scale any video to fit the aspect ratio of the terminal it's running in. ttyvideo also does not perform any smoothing operations; each character slot in the terminal corresponds to exactly one pixel in the source video, not a combination of them. Feel free to add smoothing ttyvideo! diff --git a/handle.c b/handle.c new file mode 100644 index 000000000000..1d18b8c2fc82 --- /dev/null +++ b/handle.c @@ -0,0 +1,174 @@ +#include "handle.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <signal.h> + +int error(char* message) { + + fprintf(stderr, "%s: %s\n", "ttyvideo", message); + + return 1; + +} + +static int numOptionsSpecified = 0; +static int numUniqueOptionsSpecified = 0; +static char** argumentCallMap = NULL; +static char** argumentMemStore = NULL; +static int* argumentNumStore = NULL; +static char** helpMessages = NULL; +static char* defaultArg = NULL; +static char* defaultArgHelp = NULL; + +#define MAX_ARG_SIZE 200 + +char* setDefaultArgument(char* helpText) { + + defaultArg = (char*)malloc(sizeof(char) * MAX_ARG_SIZE); + defaultArgHelp = (char*)malloc(sizeof(char) * strlen(helpText)); + strcpy(defaultArgHelp, helpText); + + defaultArg[0] = '\0'; + + return defaultArg; + +} + +#define MAX_ACCESS_MAP_SIZE 100 +#define MAX_HELP_MESSAGE_ARRAY_SIZE 100 + +char* addArgument(char* helpText, int numArguments, char* firstCall, char* secondCall) { + + if(firstCall == NULL) { + fprintf(stderr, "No option specified for parameter\n"); + return NULL; + } + if(firstCall[0] != '-') { + fprintf(stderr, "Option '%s' does not start with a '-'\n", firstCall); + return NULL; + } + if(secondCall != NULL && secondCall[0] != '-') { + fprintf(stderr, "Option '%s' does not start with a '-'\n", secondCall); + } + + if(argumentCallMap == NULL) { + argumentCallMap = (char**)malloc(sizeof(char*) * MAX_ACCESS_MAP_SIZE); + } + if(argumentMemStore == NULL) { + argumentMemStore = (char**)malloc(sizeof(char*) * MAX_ACCESS_MAP_SIZE); + } + if(argumentNumStore == NULL) { + argumentNumStore = (int*)malloc(sizeof(int*) * MAX_ACCESS_MAP_SIZE); + } + if(helpMessages == NULL) { + helpMessages = (char**)malloc(sizeof(char*) * MAX_HELP_MESSAGE_ARRAY_SIZE); + } + + // TODO: Add ability to allocate lots of these + char* argumentMemLocation = (char*)malloc(sizeof(char) * MAX_ARG_SIZE); + if(argumentMemLocation == NULL) { + fprintf(stderr, "Couldn't allocate memory to store argument to pass to %s", firstCall); + return NULL; + } + argumentMemLocation[0] = '\0'; + + int argumentAccessLocation = numOptionsSpecified++; + argumentCallMap[argumentAccessLocation] = (char*)malloc(sizeof(char) * strlen(firstCall)); + strcpy(argumentCallMap[argumentAccessLocation], firstCall); + argumentMemStore[argumentAccessLocation] = argumentMemLocation; + argumentNumStore[argumentAccessLocation] = numArguments; + + + if(secondCall == NULL) { + + helpMessages[numUniqueOptionsSpecified] = (char*)malloc(sizeof(char) * (strlen(helpText) + strlen(firstCall) + 4)); + + strcpy(helpMessages[numUniqueOptionsSpecified], firstCall); + strcat(helpMessages[numUniqueOptionsSpecified], ":\t"); + strcat(helpMessages[numUniqueOptionsSpecified], helpText); + + numUniqueOptionsSpecified++; + + return argumentMemLocation; + } + + helpMessages[numUniqueOptionsSpecified] = (char*)malloc(sizeof(char) * (strlen(helpText) + strlen(firstCall) + 2 + 4 + strlen(secondCall))); + + strcpy(helpMessages[numUniqueOptionsSpecified], firstCall); + strcat(helpMessages[numUniqueOptionsSpecified], ", "); + strcat(helpMessages[numUniqueOptionsSpecified], secondCall); + strcat(helpMessages[numUniqueOptionsSpecified], ":\t"); + strcat(helpMessages[numUniqueOptionsSpecified], helpText); + + numUniqueOptionsSpecified++; + + argumentAccessLocation = numOptionsSpecified++; + argumentCallMap[argumentAccessLocation] = (char*)malloc(sizeof(char) * strlen(secondCall)); + strcpy(argumentCallMap[argumentAccessLocation], secondCall); + argumentMemStore[argumentAccessLocation] = argumentMemLocation; + argumentNumStore[argumentAccessLocation] = numArguments; + + return argumentMemLocation; +} + +void printUsage() { + + if(helpMessages != NULL) { + register int i; + for(i = 0; i < numUniqueOptionsSpecified; ++i) { + printf("%s\n", helpMessages[i]); + } + } else { + printf("No options to display\n"); + } + +} + +int handle(int argc, char** argv) { + + register int i, j; + int numArguments; + for(i = 1; i < argc; ++i) { + for(j = 0; j < numOptionsSpecified; ++j) { + if(argv[i][0] != '-') { + if(defaultArg == NULL) { + fprintf(stderr, "Argument '%s' was passed without an option, and no default argument has been set\n", argv[0]); + return 1; + } + if(defaultArg[0] != '\0') { + fprintf(stderr, "Unrecognized option '%s'\n", argv[i]); + return 1; + } + strcpy(defaultArg, argv[i]); + break; + } + if(strcmp(argv[i], argumentCallMap[j])) + continue; + numArguments = argumentNumStore[j]; + if(numArguments) { + if(i < argc - 1 && argv[i+1][0] != '-') { + strcpy(argumentMemStore[j], argv[++i]); + } else { + fprintf(stderr, "Option '%s' requires an argument\n", argv[i]); + return 1; + } + } + else + strcpy(argumentMemStore[j], "1"); // The option is present + break; + } + if(j == numOptionsSpecified) { + fprintf(stderr, "Unrecognized option '%s'\n", argv[i]); + return 1; + } + } + + return 0; + +} + +void sig_handler(int signo) { + if(signo == SIGINT) + terminate = 1; +} diff --git a/handle.h b/handle.h new file mode 100644 index 000000000000..5a8f6806628e --- /dev/null +++ b/handle.h @@ -0,0 +1,30 @@ +#ifndef HANDLE_H +#define HANDLE_H + +#ifdef __cplusplus +extern "C" { +#endif + +char* filename; + +char* setDefaultArgument(char* helpText); + +char* addArgument(char* helpText, int numArguments, char* firstCall, char* secondCall); + +void printUsage(); + +int error(char* message); +int handle(int argc, char** argv); + +int terminate; + +#define TAKES_NO_ARGUMENTS 0 +#define TAKES_ONE_ARGUMENT 1 + +void sig_handler(int signo); + +#ifdef __cplusplus +} +#endif + +#endif // HANDLE_H diff --git a/ttyvideo.cpp b/ttyvideo.cpp new file mode 100644 index 000000000000..361eaf20373b --- /dev/null +++ b/ttyvideo.cpp @@ -0,0 +1,306 @@ +// #include <opencv2/core/core_c.h> +// #include <opencv2/videoio/videoio_c.h> +// #include <opencv2/videoio/videoio.hpp> +#include <opencv2/core.hpp> +#include <opencv2/video.hpp> +#include <opencv2/highgui.hpp> +#include <opencv2/opencv.hpp> +#include <math.h> +#include <stdlib.h> +#include <stdio.h> +#include "handle.h" +#include <sys/ioctl.h> +#include <time.h> +#include <unistd.h> +#include <signal.h> + +#ifdef __MACH__ +#include <mach/clock.h> +#include <mach/mach.h> +#endif + +#define NANO_CONV_FACTOR 1000000000 + +#define COLOR_TEXT_FORMAT "\x1B[48;05;%um\x1B[38;05;%um%c" +#define COLOR_FORMAT "\x1B[48;05;%um " +#define COLOR_RESET "\x1B[0m" + +int waitFrame(uint64_t, uint64_t); +unsigned char generateANSIColor(unsigned char, unsigned char, unsigned char); +void getTTYDims(); +int play(char*, char*, char*, int); +void getSystemTime(struct timespec*); + +int tty_width; +int tty_height; +int tty_width_custom = 0; +int tty_height_custom = 0; + +int no_interrupts = 0; + +#define C (char*) + +int main(int argc, char** argv) { + + char* filename = setDefaultArgument(C"infile"); + char* width_option = addArgument(C"Output width", TAKES_ONE_ARGUMENT, C"-w", C"--width"); + char* height_option = addArgument(C"Output height", TAKES_ONE_ARGUMENT, C"-h", C"--height"); + char* sleep_option = addArgument(C"Add a pause between loops or after plays (seconds)", TAKES_ONE_ARGUMENT, C"-s", C"--sleep"); + char* noexit_option = addArgument(C"Prevent the program for exiting", TAKES_NO_ARGUMENTS, C"--no-exit", NULL); + char* loop_option = addArgument(C"Loop videos", TAKES_NO_ARGUMENTS, C"-l", C"--loop"); + char* help_option = addArgument(C"Print usage", TAKES_NO_ARGUMENTS, C"--help", NULL); + char* nointer_option = addArgument(C"No interrupts", TAKES_NO_ARGUMENTS, C"--no-interrupts", NULL); + char* fps_option = addArgument(C"FPS", TAKES_ONE_ARGUMENT, C"--fps", NULL); + char* string_option = addArgument(C"String to place in the foreground", TAKES_ONE_ARGUMENT, C"--string", NULL); + char* info_option = addArgument(C"Print terminal dimensions", TAKES_NO_ARGUMENTS, C"-i", C"--info"); + + int argError; + argError = handle(argc, argv); + if(argError) { + error((char*)"Run ttyvideo --help for more information"); + return argError; + } + + if(help_option[0] != '\0') { + printUsage(); + return 0; + } + + if(info_option[0] != '\0') { + getTTYDims(); + printf("Height: %d\nWidth: %d\n", + tty_height, tty_width); + return 0; + } + + if(filename[0] == '\0') + return error((char*)"No input file specified"); + + getTTYDims(); // get initial dims + + tty_height_custom = atoi(height_option); + tty_width_custom = atoi(width_option); + + if(tty_height == 0 && tty_width == 0) { + if(tty_height_custom == 0 || tty_width_custom == 0) { + error((char*)"Could not detect terminal dimensions and specified dimensions are incomplete"); + return 1; + } + } else { + if(tty_height_custom > tty_height || tty_width_custom > tty_width) { + error((char*)"The specified dimensions are too large, but we will play the video anyway"); + } + } + + getTTYDims(); // get real dims -- including user specification + + int sleep_time = sleep_option[0] == '\0' ? 0 : atoi(sleep_option); + + int noexit = noexit_option[0] == '\0' ? 0 : 1; + + int loop = loop_option[0] == '\0' ? 0 : 1; + + setvbuf(stdout, NULL, _IOFBF, 0); + + signal(SIGINT, sig_handler); + + no_interrupts = nointer_option[0] == '\0' ? 0 : 1; + + register int frameNum = 0; + do { + frameNum = play(filename, string_option, fps_option, frameNum); + + if(frameNum < 0) + return 1; + + if(terminate && !no_interrupts) { + printf("\n"); + fflush(stdout); + return 0; + } + + if(sleep_time) + sleep(sleep_time); + + } while(loop && frameNum > 1); + + if((loop && frameNum == 1) || noexit) { + terminate = 0; // Terminate to 0 so we can work with it + while(true) { + sleep(1); + // When SIG_INT is sent to a frozen frame, + // re-render the frame + if(terminate) { + if(!no_interrupts) return 0; + frameNum = play(filename, string_option, fps_option, frameNum); + terminate = 0; + } + } + } + + printf("\n"); + fflush(stdout); + return 0; + +} + +int play(char* filename, char* string, char* fps_option, int subsequentPlay) { + cv::VideoCapture cap(filename); + if(!cap.isOpened()) return -1 * error((char*)"Can't read input"); + + double fps = fps_option[0] == '\0' ? cap.get(CV_CAP_PROP_FPS) : atof(fps_option); + + struct timespec start, end; + + uint64_t delta_ns; + uint64_t delayNecessary = fps == 0 || isnan(fps) ? 0 : NANO_CONV_FACTOR/fps; + + register int frameNum = 0; + + register int width, height, nchannels, step, offset; + register int i, j; + register unsigned char r_ch, b_ch, g_ch; + cv::Mat frame; + int ansiColor, ansiTextColor; + unsigned char* data; + + int stringLength = strlen(string); + + int stopAfterFrame = 0; + + for(;;) { + + getSystemTime(&start); + + if(!frame.empty() || subsequentPlay) { + + cap >> frame; + + if(frame.empty()) break; + + for(i = 0; i < tty_height - 1; ++i) + printf("\x1B[F"); + + } else { + + cap >> frame; + + if(frame.empty()) break; + + } + + if(frame.depth() != CV_8U) { + return -1 * error((char*)"Frame has incorrect depth"); + } + + width = frame.cols; + height = frame.rows; + nchannels = frame.channels(); + step = width * nchannels; + + getTTYDims(); + + register int stringIter = 0; + + for(i = 0; i < tty_height; ++i) { + data = (unsigned char*)(frame.data + ((int)((float)i*height/tty_height)*step)); + for(j = 0; j < tty_width; ++j) { + offset = (int)((float)j*width/tty_width) * nchannels; + b_ch = data[offset]; + g_ch = data[offset+1]; + r_ch = data[offset+2]; + + ansiColor = generateANSIColor(r_ch, g_ch, b_ch); + + if(stringLength) { + ansiTextColor = b_ch + g_ch + r_ch > 0x17F ? generateANSIColor(r_ch - 50, g_ch - 50, b_ch - 50) : + generateANSIColor(r_ch + 50, g_ch + 50, b_ch + 50); + + printf(COLOR_TEXT_FORMAT, ansiColor, ansiTextColor, string[stringIter++%stringLength]); + } else { + printf(COLOR_FORMAT, ansiColor); + } + } + printf(COLOR_RESET); + if(i < tty_height - 1) { + printf("\n"); + } else { + printf("\x1B[m"); + fflush(stdout); + } + } + + if(stopAfterFrame) { + break; + } + + if(terminate && !no_interrupts) { + stopAfterFrame = 1; + } + + getSystemTime(&end); + + delta_ns = (end.tv_sec - start.tv_sec) * NANO_CONV_FACTOR + (end.tv_nsec - start.tv_nsec); + + waitFrame(delayNecessary, delta_ns); + + frameNum++; + + } + + return frameNum; + +} + +int waitFrame(uint64_t delayNecessary, uint64_t delta_ns) { + + struct timespec ts; + long totalNanoSec; + + totalNanoSec = (delayNecessary - delta_ns); + + if(totalNanoSec < 0) return 1; + + ts.tv_sec = totalNanoSec/NANO_CONV_FACTOR; + ts.tv_nsec = totalNanoSec%NANO_CONV_FACTOR; + nanosleep(&ts, NULL); + + return 0; + +} + +unsigned char generateANSIColor(unsigned char r, unsigned char g, unsigned char b) { + + return 16 + (36 * lround(r*5.0/256)) + (6 * lround(g*5.0/256)) + lround(b*5.0/256); + +} + +void getTTYDims() { + + struct winsize w; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); + tty_width = tty_width_custom > 0 ? tty_width_custom : w.ws_col; + tty_height = tty_height_custom > 0 ? tty_height_custom : w.ws_row; + +} + +void getSystemTime(struct timespec* tv) { + + #ifdef __MACH__ + + clock_serv_t cclock; + mach_timespec_t mts; + host_get_clock_service(mach_host_self(), CALENDAR_CLOCK, &cclock); + clock_get_time(cclock, &mts); + mach_port_deallocate(mach_task_self(), cclock); + + tv->tv_sec = mts.tv_sec; + tv->tv_nsec = mts.tv_nsec; + + #else + + clock_gettime(CLOCK_REALTIME, tv); + + #endif + +} |