diff --git a/README.md b/README.md index e9e333a..b97550c 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,12 @@ Mileage may vary depending on how fast your terminal is. In my testing, I've fou .\tvp filename [threshold] ``` -The threshold has to be an integer from 0 to 255, and defaults to 10. The threshold affects how much the colour of a certain pixel has to change before it will be redrawn. A lower threshold results in more redraws in most cases, and leads to choppy video. Unfortunately I can't be bothered to rewrite this to decode video using FFmpeg so you'll have to build it with OpenCV. +The threshold has to be an integer from 0 to 255, and defaults to 10. The threshold affects how much the colour of a certain pixel has to change before it will be redrawn. A lower threshold results in more redraws in most cases, and leads to choppy video. It relies on [ffmpeg](https://www.ffmpeg.org/) in order to decode the video input. Built on Manjaro with this command: ```sh -g++ src/main.cpp -std=c++17 -O3 -o tvp `pkg-config --cflags --libs opencv4` +g++ src/main.cpp src/video.cpp -Iinc/ -std=c++17 -O3 -o tvp -lavformat -lavcodec -lavutil -lswscale ``` Below is a preview of how it looks: diff --git a/inc/video.h b/inc/video.h new file mode 100644 index 0000000..707c911 --- /dev/null +++ b/inc/video.h @@ -0,0 +1,58 @@ +// +// Created by orange on 24/2/22. +// + +#ifndef VIDPLAYER_VIDEO_H +#define VIDPLAYER_VIDEO_H + +#include + +extern "C" { + #include + #include + #include + #include +} + +class video { +public: + video(char filename[], int w, int h); + explicit video(char filename[]) : video(filename, -1, -1) {}; + ~video(); + + void setResize(int w, int h); + [[nodiscard]] bool isOpened() const; + double get_fps(); + [[nodiscard]] int get_width() const; + [[nodiscard]] int get_height() const; + [[nodiscard]] int get_dst_buf_size() const; + int get_frame(int dst_w, int dst_h, const char* dst_frame); + +private: + AVFormatContext* inctx = nullptr; + AVCodecContext* codec; + AVCodec* vcodec = nullptr; + AVStream* vstrm = nullptr; + AVFrame* frame = nullptr; + SwsContext* swsctx = nullptr; + bool opened = false; + + int vstrm_idx; + + int src_width; + int src_height; + int dst_width; + int dst_height; + + AVFrame* decframe = nullptr; + bool end_of_stream_pkt = false, end_of_stream_enc = false; + AVPacket* pkt = nullptr; + + bool alloc = false; + + const AVPixelFormat dst_pix_fmt = AV_PIX_FMT_BGR24; + char errbuf[200]{}; +}; + + +#endif //VIDPLAYER_VIDEO_H diff --git a/src/main.cpp b/src/main.cpp index 8168cb1..d65dfca 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -8,6 +7,8 @@ #include #include +#include "video.h" + #define CHANGE_THRESHOLD 3 #define CHAR_Y 4 @@ -22,54 +23,54 @@ const char characters[DIFF_CASES][4] = {"\u2584", // bottom half block "\u2597", // bottom right quarter "\u259e", // diagonal "\u2582", // lower quarter block - "\u2586", + "\u2586", // lower 3 quarters block "\u258e", - "\u258a"};// lower 3 quarters block + "\u258a"}; -const int pixelmap[DIFF_CASES][CHAR_Y*CHAR_X] = {{0, 0, 0, 0, - 0, 0, 0, 0, - 1, 1, 1, 1, - 1, 1, 1, 1}, - {0, 0, 1, 1, - 0, 0, 1, 1, - 0, 0, 1, 1, - 0, 0, 1, 1}, - {1, 1, 0, 0, - 1, 1, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0}, - {0, 0, 1, 1, - 0, 0, 1, 1, - 0, 0, 0, 0, - 0, 0, 0, 0}, - {0, 0, 0, 0, - 0, 0, 0, 0, - 1, 1, 0, 0, - 1, 1, 0, 0}, - {0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 1, 1, - 0, 0, 1, 1}, - {0, 0, 1, 1, - 0, 0, 1, 1, - 1, 1, 0, 0, - 1, 1, 0, 0}, - {0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 1, 1, 1, 1}, - {0, 0, 0, 0, - 1, 1, 1, 1, - 1, 1, 1, 1, - 1, 1, 1, 1}, - {1, 0, 0, 0, - 1, 0, 0, 0, - 1, 0, 0, 0, - 1, 0, 0, 0}, - {1, 1, 1, 0, - 1, 1, 1, 0, - 1, 1, 1, 0, - 1, 1, 1, 0}}; +const int pixelmap[DIFF_CASES][CHAR_Y * CHAR_X] = {{0, 0, 0, 0, + 0, 0, 0, 0, + 1, 1, 1, 1, + 1, 1, 1, 1}, + {0, 0, 1, 1, + 0, 0, 1, 1, + 0, 0, 1, 1, + 0, 0, 1, 1}, + {1, 1, 0, 0, + 1, 1, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0}, + {0, 0, 1, 1, + 0, 0, 1, 1, + 0, 0, 0, 0, + 0, 0, 0, 0}, + {0, 0, 0, 0, + 0, 0, 0, 0, + 1, 1, 0, 0, + 1, 1, 0, 0}, + {0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 1, 1, + 0, 0, 1, 1}, + {0, 0, 1, 1, + 0, 0, 1, 1, + 1, 1, 0, 0, + 1, 1, 0, 0}, + {0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 1, 1, 1, 1}, + {0, 0, 0, 0, + 1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 1, 1}, + {1, 0, 0, 0, + 1, 0, 0, 0, + 1, 0, 0, 0, + 1, 0, 0, 0}, + {1, 1, 1, 0, + 1, 1, 1, 0, + 1, 1, 1, 0, + 1, 1, 1, 0}}; #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN @@ -83,10 +84,9 @@ const int pixelmap[DIFF_CASES][CHAR_Y*CHAR_X] = {{0, 0, 0, 0, #endif // Windows/Linux -using namespace cv; - void get_terminal_size(int &width, int &height) { - width = -1; height = -1; + width = -1; + height = -1; #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) CONSOLE_SCREEN_BUFFER_INFO csbi; GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); @@ -121,9 +121,9 @@ int im_w, im_h; double scale_factor = 0.0; int small_dims[2]; -Mat frame; -Mat resized; -Mat old; +char *frame; +char *old; +bool alloc = false; int diff = 0; int pixel[CHAR_Y][CHAR_X][3]; @@ -132,7 +132,7 @@ bool begin = true; int r, c; -char printbuf[100000000]; +char printbuf[50000000]; const char *shapechar; int count = 0, curr_frame = 0;; double fps; @@ -165,7 +165,7 @@ int pixelbg[3], pixelchar[3]; int prevpixel[3] = {1000, 1000, 1000}; int sx = 4, sy = 8; -int skipy = sy/CHAR_Y, skipx = sx/CHAR_X; +int skipy = sy / CHAR_Y, skipx = sx / CHAR_X; void terminateProgram([[maybe_unused]] int sig_num) { videostop = std::chrono::steady_clock::now(); @@ -196,7 +196,7 @@ int main(int argc, char *argv[]) { #endif if (argc > 2) diffthreshold = std::stoi(argv[2], nullptr, 10); diffthreshold = std::max(std::min(255, diffthreshold), 0); - VideoCapture cap(argv[1]); + video cap(argv[1]); // Check if camera opened successfully if (!cap.isOpened()) { printf("\u001b[0mError opening video stream or file\n"); @@ -204,14 +204,12 @@ int main(int argc, char *argv[]) { return -1; } - fps = cap.get(CAP_PROP_FPS); + fps = cap.get_fps(); period = (int) (1000000.0 / fps); start = std::chrono::steady_clock::now(); while (true) { count++; curr_frame++; - // Capture frame-by-frame - cap >> frame; get_terminal_size(curr_w, curr_h); if (curr_w != orig_w || curr_h != orig_h) { @@ -223,8 +221,8 @@ int main(int argc, char *argv[]) { msg_y = h; h *= sy; w *= sx; - im_w = frame.cols; - im_h = frame.rows; + im_w = cap.get_width(); + im_h = cap.get_height(); scale_factor = std::min((double) w / (double) im_w, (double) h / (double) im_h); small_dims[0] = int((double) im_w * scale_factor); small_dims[1] = int((double) im_h * scale_factor); @@ -240,7 +238,7 @@ int main(int argc, char *argv[]) { printf("\u001b[?25l"); printf("terminal dimensions: (w %4d, h %4d)\n", curr_w, curr_h); printf("frame dimensions: (w %4d, h %4d)\n", im_w, im_h); - printf("display dimensions: (w %4d, h %4d)\n", small_dims[0], small_dims[1] / (sy/sx)); + printf("display dimensions: (w %4d, h %4d)\n", small_dims[0], small_dims[1] / (sy / sx)); printf("scaling: %f\n", scale_factor); printf("frames per second: %f\n", fps); fflush(stdout); @@ -248,14 +246,26 @@ int main(int argc, char *argv[]) { start = std::chrono::steady_clock::now(); videostart = std::chrono::steady_clock::now(); } + if (alloc) { + std::free(frame); + std::free(old); + } + cap.setResize(small_dims[0], small_dims[1]); + frame = (char *) std::malloc(cap.get_dst_buf_size()); + old = (char *) std::malloc(cap.get_dst_buf_size()); + alloc = true; + + memset(old, 0, sizeof(*old)); + printf("\u001b[0;0H\u001b[48;2;0;0;0m"); for (int i = 0; i < curr_w * curr_h; i++) printf(" "); } + int ret = cap.get_frame(small_dims[0], small_dims[1], frame); + stop = std::chrono::steady_clock::now(); - elapsed = (int)std::chrono::duration_cast(stop - videostart).count(); - int frame_time = (int)std::chrono::duration_cast( - stop - start).count(); + elapsed = (int) std::chrono::duration_cast(stop - videostart).count(); + int frame_time = (int) std::chrono::duration_cast(stop - start).count(); start = std::chrono::steady_clock::now(); total_time = elapsed; @@ -268,13 +278,16 @@ int main(int argc, char *argv[]) { } if (curr_frame * period - elapsed > 0) - std::this_thread::sleep_until(std::chrono::microseconds(curr_frame * period - elapsed - frame10_time/frametimes.size())+stop); + std::this_thread::sleep_until( + std::chrono::microseconds(curr_frame * period - elapsed - frame10_time / frametimes.size()) + + stop); else { skip = (double) elapsed / (double) period - (double) curr_frame; - for (int i = 0; i < std::floor(skip); i++) cap >> frame; + for (int i = 0; i < std::floor(skip); i++) ret = cap.get_frame(small_dims[0], small_dims[1], frame); dropped += std::floor(skip); curr_frame += std::floor(skip); - std::this_thread::sleep_until(std::chrono::microseconds(curr_frame * period - frame10_time/frametimes.size()) + videostart); + std::this_thread::sleep_until( + std::chrono::microseconds(curr_frame * period - frame10_time / frametimes.size()) + videostart); } printf("\u001b[%d;%dH\u001b[48;2;0;0;0;38;2;255;255;255m fps: %5.2f | avg_fps: %5.2f | print: %6.2fms | dropped: %5d | curr_frame: %5d ", @@ -288,29 +301,25 @@ int main(int argc, char *argv[]) { prevpixel[2] = 1000; // If the frame is empty, break immediately - if (frame.empty()) { + if (ret < 0) { printf("\u001b[0mError reading video stream or file\n"); break; } - resize(frame, resized, Size(small_dims[0], small_dims[1]), INTER_LINEAR); - - if (refresh) old = resized.clone(); - r = -1; c = -1; - Vec3b *row[CHAR_Y]; - Vec3b *oldrow[CHAR_Y]; - for (int ay = 0; ay < resized.rows / sy; ay++) { - for (int x = 0; x < resized.cols / sx; x++) { + char *row[CHAR_Y]; + char *oldrow[CHAR_Y]; + for (int ay = 0; ay < cap.get_height() / sy; ay++) { + for (int x = 0; x < cap.get_width() / sx; x++) { for (int i = 0; i < CHAR_Y; i++) { - row[i] = resized.ptr(ay * sy + i*skipy); - oldrow[i] = old.ptr(ay * sy + i*skipy); + row[i] = frame + (ay * sy + i * skipy) * 3 * cap.get_width(); + oldrow[i] = old + (ay * sy + i * skipy) * 3 * cap.get_width(); } for (int i = 0; i < CHAR_Y; i++) for (int j = 0; j < CHAR_X; j++) for (int k = 0; k < 3; k++) - pixel[i][j][k] = row[i][x * sx + j*skipx][k]; + pixel[i][j][k] = (unsigned char) (*(row[i] + (x * sx + j * skipx) * 3 + k)); diff = 0; if (refresh) { @@ -319,15 +328,20 @@ int main(int argc, char *argv[]) { for (int i = 0; i < CHAR_Y; i++) for (int j = 0; j < CHAR_X; j++) for (int k = 0; k < 3; k++) - diff = std::max(diff, std::abs(oldrow[i][x * sx + j*skipx][k] - pixel[i][j][k])); + diff = std::max(diff, std::abs( + (unsigned char) (*(oldrow[i] + (x * sx + j * skipx) * 3 + k)) - + pixel[i][j][k])); } if (diff >= diffthreshold) { - for (int & case_it : cases) case_it = 0; + for (int &case_it: cases) case_it = 0; - for (int k = 0; k < 3;k++) { - for (int case_it = 0;case_it < sizeof(cases)/sizeof(cases[0]);case_it++) { - min_fg = 256; min_bg = 256; max_fg = 0; max_bg = 0; + for (int k = 0; k < 3; k++) { + for (int case_it = 0; case_it < sizeof(cases) / sizeof(cases[0]); case_it++) { + min_fg = 256; + min_bg = 256; + max_fg = 0; + max_bg = 0; for (int i = 0; i < CHAR_Y; i++) for (int j = 0; j < CHAR_X; j++) { if (pixelmap[case_it][i * CHAR_X + j]) { @@ -344,7 +358,7 @@ int main(int argc, char *argv[]) { mindiff = 256; case_min = 0; - for (int case_it = 0;case_it < sizeof(cases)/sizeof(cases[0]);case_it++) { + for (int case_it = 0; case_it < sizeof(cases) / sizeof(cases[0]); case_it++) { if (cases[case_it] < mindiff) { case_min = case_it; mindiff = cases[case_it]; @@ -359,7 +373,8 @@ int main(int argc, char *argv[]) { for (int k = 0; k < 3; k++) { int bg_count = 0, fg_count = 0; - pixelchar[k] = 0; pixelbg[k] = 0; + pixelchar[k] = 0; + pixelbg[k] = 0; for (int i = 0; i < CHAR_Y; i++) for (int j = 0; j < CHAR_X; j++) { if (pixelmap[case_min][i * CHAR_X + j]) { @@ -377,23 +392,23 @@ int main(int argc, char *argv[]) { } if (diffbg < CHANGE_THRESHOLD) { - for (int k = 0;k < 3;k++) pixelbg[k] = prevpixelbg[k]; + for (int k = 0; k < 3; k++) pixelbg[k] = prevpixelbg[k]; bgsame = true; } else - for (int k = 0;k < 3;k++) prevpixelbg[k] = pixelbg[k]; + for (int k = 0; k < 3; k++) prevpixelbg[k] = pixelbg[k]; if (diffpixel < CHANGE_THRESHOLD) { - for (int k = 0;k < 3;k++) pixelchar[k] = prevpixel[k]; + for (int k = 0; k < 3; k++) pixelchar[k] = prevpixel[k]; pixelsame = true; } else - for (int k = 0;k < 3;k++) prevpixel[k] = pixelchar[k]; + for (int k = 0; k < 3; k++) prevpixel[k] = pixelchar[k]; for (int k = 0; k < 3; k++) for (int i = 0; i < CHAR_Y; i++) for (int j = 0; j < CHAR_X; j++) { if (pixelmap[case_min][i * CHAR_X + j]) - oldrow[i][x * sx + j*skipx][k] = pixelchar[k]; + *(oldrow[i] + (x * sx + j * skipx) * 3 + k) = (char) pixelchar[k]; else - oldrow[i][x * sx + j*skipx][k] = pixelbg[k]; + *(oldrow[i] + (x * sx + j * skipx) * 3 + k) = (char) pixelbg[k]; } if (r != ay || c != x) { @@ -424,12 +439,10 @@ int main(int argc, char *argv[]) { printtime = std::chrono::steady_clock::now(); fflush(stdout); - printing_time = (int) std::chrono::duration_cast(std::chrono::steady_clock::now() - printtime).count(); + printing_time = (int) std::chrono::duration_cast( + std::chrono::steady_clock::now() - printtime).count(); total_printing_time += printing_time; } - - // When everything done, release the video capture object - cap.release(); } else { printf("\u001b[0mfile not found\n"); fflush(stdout); diff --git a/src/video.cpp b/src/video.cpp new file mode 100644 index 0000000..9a6981a --- /dev/null +++ b/src/video.cpp @@ -0,0 +1,151 @@ +// +// Created by orange on 24/2/22. +// + +#include "video.h" + +video::video(char filename[], int w, int h) { + int ret; + errbuf[0] = '\0'; + ret = avformat_open_input(&inctx, filename, nullptr, nullptr); + if (ret < 0) { + av_make_error_string(errbuf, sizeof(errbuf), ret); + fprintf(stderr, "fail to avformat_open_input(%s): %s\n", filename, errbuf); + return; + } + + ret = avformat_find_stream_info(inctx, nullptr); + if (ret < 0) { + av_make_error_string(errbuf, sizeof(errbuf), ret); + fprintf(stderr, "fail to avformat_find_stream_info: %s\n", errbuf); + return; + } + ret = av_find_best_stream(inctx, AVMEDIA_TYPE_VIDEO, -1, -1, &vcodec, 0); + if (ret < 0) { + av_make_error_string(errbuf, sizeof(errbuf), ret); + fprintf(stderr, "fail to av_find_best_stream: %s\n", errbuf); + return; + } + vstrm_idx = ret; + vstrm = inctx->streams[vstrm_idx]; + + codec = avcodec_alloc_context3(vcodec); + ret = avcodec_parameters_to_context(codec, vstrm->codecpar); + if (ret < 0) { + av_make_error_string(errbuf, sizeof(errbuf), ret); + fprintf(stderr, "fail to avcodec_parameters_to_context: %s\n", errbuf); + return; + } + + ret = avcodec_open2(codec, vcodec, nullptr); + if (ret < 0) { + av_make_error_string(errbuf, sizeof(errbuf), ret); + fprintf(stderr, "fail to avcodec_open2: %s\n", errbuf); + return; + } + + src_width = codec->width; + src_height = codec->height; + + if (w == -1 || h == -1) { + dst_width = src_width; + dst_height = src_height; + } else { + dst_width = w; + dst_height = h; + } + + frame = av_frame_alloc(); + decframe = av_frame_alloc(); + pkt = av_packet_alloc(); + + setResize(dst_width, dst_height); + + opened = true; +} + +double video::get_fps() { + return av_q2d(vstrm->r_frame_rate); +} + +int video::get_width() const { + return dst_width; +} + +int video::get_height() const { + return dst_height; +} + +int video::get_frame(int dst_w, int dst_h, const char* dst_frame) { + int ret; + // read packet from input file + if (dst_w != dst_width || dst_h != dst_height) return 1; + do { + if (!end_of_stream_pkt) { + ret = av_read_frame(inctx, pkt); + end_of_stream_pkt = (AVERROR_EOF == ret); + if (end_of_stream_pkt) avcodec_send_packet(codec, nullptr); + if (ret < 0 && ret != AVERROR_EOF) { + av_make_error_string(errbuf, sizeof(errbuf), ret); + fprintf(stderr, "fail to av_read_frame: %s\n", errbuf); + av_packet_unref(pkt); + return -1; + } + } + + if (!end_of_stream_pkt && pkt->stream_index == vstrm_idx) { + ret = avcodec_send_packet(codec, pkt); + if (ret < 0) { + av_make_error_string(errbuf, sizeof(errbuf), ret); + fprintf(stderr, "fail to av_send_packet: %s\n", errbuf); + } + } + + ret = avcodec_receive_frame(codec, decframe); + if (ret < 0 && ret != AVERROR_EOF && ret != AVERROR(EAGAIN)) { + av_make_error_string(errbuf, sizeof(errbuf), ret); + fprintf(stderr, "fail to av_receive_frame: %s\n", errbuf); + } + end_of_stream_enc = (AVERROR_EOF == ret); + + av_packet_unref(pkt); + } while(ret == AVERROR(EAGAIN) && !end_of_stream_enc); + if (end_of_stream_enc) return -1; + + sws_scale(swsctx, decframe->data, decframe->linesize, 0, decframe->height, frame->data, frame->linesize); + //char *it = (char *)dst_frame; + av_image_copy_to_buffer((uint8_t *) dst_frame, get_dst_buf_size(), frame->data, frame->linesize, dst_pix_fmt, dst_width, dst_height, 1); + //printf("copy %p\n", dst_frame); + return 0; +} + +bool video::isOpened() const { + return opened; +} + +void video::setResize(int w, int h) { + dst_width = w; + dst_height = h; + swsctx = sws_getCachedContext(swsctx, codec->width, codec->height, codec->pix_fmt, + dst_width, dst_height, dst_pix_fmt, SWS_BICUBIC, nullptr, nullptr, nullptr); + if (!swsctx) { + fprintf(stderr, "fail to sws_getCachedContext\n"); + return; + } + + if (alloc) av_freep(&frame->data[0]); + av_image_alloc(frame->data, frame->linesize, dst_width, dst_height, dst_pix_fmt, 16); + printf("after av alloc %p\n", frame->data); + alloc = true; +} + +video::~video() { + av_freep(&frame->data); + av_freep(&decframe->data); + av_frame_free(&frame); + av_frame_free(&decframe); +} + +int video::get_dst_buf_size() const { + return dst_height*dst_width*3+50; +} diff --git a/tvp b/tvp index c2e6743..c802a40 100755 Binary files a/tvp and b/tvp differ