recorder writes video frames to folder

This commit is contained in:
Frank DeMarco 2019-05-16 03:51:36 -04:00
parent 1dbb2a2e1d
commit 321d9df1be
18 changed files with 325 additions and 161 deletions

View File

@ -1,8 +1,9 @@
/***
reset, pause, auto reset, analog d-pad, gamepad config, any key, confirm exit
game, screen wipes
game, screen wipes, screen offset, screen scale
:) SWEATY HANDS :) OILY SNACKS :) AND BAD HYGIENE :)
*surf's up broccoli* <it's surfing time but it's treacherous>
***/
@ -65,56 +66,6 @@ int link_shader(GLuint program)
return 0;
}
SDL_Surface* get_screen_surface(SDL_Window *window)
{
int w, h;
SDL_GetWindowSize(window, &w, &h);
unsigned char* pixels = new unsigned char[24 * w * h];
GLenum format;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
format = GL_RGB;
#else
format = GL_BGR;
#endif
glReadPixels(0, 0, w, h, format, GL_UNSIGNED_BYTE, pixels);
SDL_Surface *surface = zoomSurface(
SDL_CreateRGBSurfaceFrom(pixels, w, h, 24, 3 * w,
0, 0, 0, 0), 1, -1, SMOOTHING_OFF);
delete[] pixels;
return surface;
}
void capture_screen(SDL_Window *window)
{
SDL_Surface *surface = get_screen_surface(window);
IMG_SavePNG(surface, "screen.png");
printf("saved png to screen.png\n");
SDL_FreeSurface(surface);
}
void start_recording(bool *is_recording)
{
*is_recording = true;
printf("start recording\n");
}
void end_recording(std::list<SDL_Surface*> frames, bool *is_recording)
{
*is_recording = false;
printf("end recording\n");
SDL_Surface *frame;
int ii = 0;
while (not frames.empty())
{
frame = frames.front();
char path[22];
sprintf(path, "frames/%03i.png", ii++);
IMG_SavePNG(frame, path);
frames.pop_front();
SDL_FreeSurface(frame);
}
}
GLuint get_gl_texture_from_surface(SDL_Surface *surface, GLint mipmap_filter)
{
GLuint id;
@ -162,12 +113,9 @@ struct Demo : Game
{
SDL_Texture *grass_texture;
int recording_capture_framerate = 100, frame_time_overflow = 0,
capture_time_overflow = 0, frame_count = 0, frame_count_timestamp,
last_capture_timestamp;
std::list<SDL_Surface*> frames;
bool is_recording = false, right_active = false, down_active = false,
left_active = false, up_active = false;
int frame_count = 0, frame_count_timestamp;
bool right_active = false, down_active = false, left_active = false,
up_active = false;
SDL_Event event;
GLuint vbo, space_texture_id, mvp_id, framerate_texture_id, flat_program,
world_program, fake_texture_id;
@ -341,7 +289,7 @@ struct Demo : Game
glBindTexture(GL_TEXTURE_2D, space_texture_id);
glUniform1i(sampler_uniform_id, 0);
glDepthFunc(GL_LESS);
frame_count_timestamp = last_capture_timestamp = SDL_GetTicks();
frame_count_timestamp = SDL_GetTicks();
framerate_texture_id = get_gl_texture_from_surface(
get_framerate_indicator_surface(frame_count), GL_LINEAR);
}
@ -367,18 +315,7 @@ struct Demo : Game
// {
// if (event.type == SDL_KEYDOWN)
// {
// if (event.key.keysym.sym == SDLK_F10)
// {
// if (not is_recording)
// {
// start_recording(&is_recording);
// }
// else
// {
// end_recording(frames, &is_recording);
// }
// }
// else if (event.key.keysym.sym == SDLK_F11)
// if (event.key.keysym.sym == SDLK_F11)
// {
// if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN)
// {
@ -441,20 +378,6 @@ struct Demo : Game
// }
// }
// }
if (is_recording and ticks - last_capture_timestamp + capture_time_overflow >
recording_capture_framerate)
{
frames.push_back(get_screen_surface(window));
printf("added frame at %i\n", ticks);
capture_time_overflow = ticks - last_capture_timestamp + capture_time_overflow -
recording_capture_framerate;
last_capture_timestamp = ticks;
for (int ii = 1; capture_time_overflow > recording_capture_framerate;
ii++, capture_time_overflow -= recording_capture_framerate)
{
fprintf(stderr, "lost %i frame(s) during capture\n", ii);
}
}
if (is_gl_context)
{
// glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);

View File

@ -36,18 +36,21 @@ $(SDLGFX2_DIR)%.o: $(SDLGFX2_DIR)%.c $(SDLGFX2_DIR)%.h
$(GLEW_DIR)%.o: $(GLEW_DIR)%.c $(GLEW_DIR)%.h
$(CC_LINUX) $(CFLAGS) $< -o $@
$(SFW_SRC_DIR)Sprite.o: $(addprefix $(SFW_SRC_DIR),Game.*pp Location.*pp)
$(SFW_SRC_DIR)Game.o: $(addprefix $(SFW_SRC_DIR),Sprite.*pp Configuration.*pp Delegate.*pp Display.*pp Recorder.*pp)
$(SFW_SRC_DIR)Node.o: $(addprefix $(SFW_SRC_DIR),Game.*pp Configuration.*pp)
$(SFW_SRC_DIR)Recorder.o: $(addprefix $(SFW_SRC_DIR),extension.*pp)
$(SFW_SRC_DIR)%.o: $(addprefix $(SFW_SRC_DIR),%.cpp %.hpp Node.*pp)
$(SFW_SRC_DIR)Sprite.o: $(addprefix $(SFW_SRC_DIR),Game.*pp Location.*pp Node.*pp)
$(SFW_SRC_DIR)Game.o: $(addprefix $(SFW_SRC_DIR),Sprite.*pp Configuration.*pp Delegate.*pp Display.*pp \
Recorder.*pp Node.*pp)
$(SFW_SRC_DIR)Node.o: $(addprefix $(SFW_SRC_DIR),Game.*pp Configuration.*pp Node.*pp)
$(SFW_SRC_DIR)Animation.o: $(addprefix $(SFW_SRC_DIR),Timer.*pp)
$(SFW_SRC_DIR)Recorder.o: $(addprefix $(SFW_SRC_DIR),extension.*pp Node.*pp)
$(SFW_SRC_DIR)%.o: $(addprefix $(SFW_SRC_DIR),%.cpp %.hpp)
$(CPPC_LINUX) $(CPP_FLAGS) $(SDL_FLAGS) $< -o $@
Demo.o: Demo.cpp Demo.hpp $(addprefix $(SFW_SRC_DIR),Sprite.*pp Node.*pp Game.*pp Location.*pp Input.*pp Recorder.*pp)
Demo.o: Demo.cpp Demo.hpp $(addprefix $(SFW_SRC_DIR),Sprite.*pp Node.*pp Game.*pp Location.*pp Input.*pp \
Recorder.*pp Timer.*pp Animation.*pp extension.*pp)
$(CPPC_LINUX) $(CPP_FLAGS) $(SDL_FLAGS) $< -o $@
linux: Demo.o $(addprefix $(SFW_SRC_DIR),Sprite.o Node.o Game.o Location.o Configuration.o Input.o Delegate.o \
Display.o Recorder.o extension.o) \
Display.o Recorder.o Timer.o Animation.o extension.o) \
$(GLEW_DIR)glew.o $(addprefix $(SDLGFX2_DIR),SDL2_rotozoom.o SDL2_gfxPrimitives.o)
$(CPPC_LINUX) $(LFLAGS) -D__LINUX__ $^ -lGL -lSDL2_image -lSDL2_ttf -lSDL2_mixer -lstdc++fs -o demo

View File

@ -5,7 +5,8 @@
},
"path":
{
"screenshots": "local/screenshots"
"screenshots": "local/screenshots",
"video": "local/video"
},
"gamepad":
{

70
src/Animation.cpp Normal file
View File

@ -0,0 +1,70 @@
#include "Animation.hpp"
void Animation::play(int delay, bool play_once)
{
this->delay = delay;
playing = true;
paused = false;
previous_step_time = timer.elapsed;
overflow = 0;
count = 0;
ending = play_once;
if (delay <= 0)
{
timer.toggle(true);
}
}
void Animation::play_once(int delay)
{
play(delay, true);
}
void Animation::pause()
{
timer.toggle(false);
paused = true;
}
void Animation::unpause()
{
timer.toggle(true);
paused = false;
}
void Animation::reset()
{
timer.toggle(false);
playing = false;
timer.reset();
}
void Animation::update()
{
timer.update();
if (playing and not paused)
{
if (delay > 0)
{
delay -= timer.frame_duration;
if (delay <= 0)
{
timer.toggle(true);
}
}
if (delay <= 0)
{
if (timer.elapsed - previous_step_time + overflow > framerate)
{
overflow = timer.elapsed - previous_step_time + overflow - framerate;
previous_step_time = timer.elapsed;
step();
count++;
if (ending)
{
reset();
}
}
}
}
}

36
src/Animation.hpp Normal file
View File

@ -0,0 +1,36 @@
#ifndef Animation_h_
#define Animation_h_
#include <vector>
#include <functional>
#include <algorithm>
#include "Timer.hpp"
typedef std::function<void()> callback;
struct Animation
{
bool playing = false, ending = false, paused = false;
int previous_step_time = 0, delay = 0, overflow = 0, count = 0, framerate;
callback step;
Timer timer = Timer();
template<typename T>
Animation(void(T::*f)(), T* o, int framerate = 0) : framerate(framerate)
{
step = std::bind(f, o);
timer.toggle(false);
}
void play(int = 0, bool = false);
void play_once(int = 0);
void pause();
void unpause();
void reset();
void update();
};
#endif

View File

@ -15,20 +15,24 @@ void Configuration::set_defaults()
sys_config["keys"] = {
{"record", {"CTRL", "SHIFT", "f10"}},
{"screenshot", "f9"},
{"action", " "},
{"action", "space"},
{"up", "up"},
{"right", "right"},
{"down", "down"},
{"left", "left"}
{"left", "left"},
{"pause", "enter"},
{"fullscreen", {"ALT", "enter"}}
};
sys_config["path"] = {
{"screenshots", "."}
{"screenshots", "."},
{"video", "."}
};
sys_config["display"] = {
{"dimensions", {640, 480}},
{"screenshot-prefix", "screenshot-"},
{"screenshot-extension", ".png"},
{"screenshot-zfill", 5}
{"screenshot-zfill", 5},
{"recording-framerate", 100}
};
}

View File

@ -9,3 +9,27 @@ glm::ivec2 Display::get_window_size()
SDL_GetWindowSize(get_root()->window, &size.x, &size.y);
return size;
}
void Display::get_screen_pixels(unsigned char* pixels, int w, int h, int x, int y)
{
GLenum format;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
format = GL_RGB;
#else
format = GL_BGR;
#endif
glReadPixels(x, y, w, h, format, GL_UNSIGNED_BYTE, pixels);
}
SDL_Surface* Display::get_screen_surface()
{
glm::ivec2 size = get_window_size();
unsigned char* pixels = new unsigned char[bpp / 8 * size.x * size.y];
get_screen_pixels(pixels, size.x, size.y);
SDL_Surface* surface = SDL_CreateRGBSurfaceFrom(
pixels, size.x, size.y, bpp, bpp / 8 * size.x, 0, 0, 0, 0);
SDL_Surface* zoomed_surface = zoomSurface(surface, 1, -1, SMOOTHING_OFF);
SDL_FreeSurface(surface);
delete[] pixels;
return zoomed_surface;
}

View File

@ -4,6 +4,14 @@
#define GLM_ENABLE_EXPERIMENTAL
#include "glm/vec2.hpp"
#define GL_GLEXT_PROTOTYPES
#define GLEW_STATIC
#include "glew/glew.h"
#include <SDL_image.h>
#include "sdl2-gfx/SDL2_gfxPrimitives.h"
#include "sdl2-gfx/SDL2_rotozoom.h"
#include "SDL.h"
#include "Node.hpp"
@ -11,8 +19,12 @@
struct Display : Node
{
const static int bpp = 24;
Display(Node*);
glm::ivec2 get_window_size();
SDL_Surface* get_screen_surface();
void get_screen_pixels(unsigned char*, int, int, int = 0, int = 0);
};

View File

@ -130,6 +130,7 @@ void Game::run()
last_frame_length = ticks - last_frame_timestamp;
frame_time_overflow = last_frame_length + frame_time_overflow - frame_length;
last_frame_timestamp = ticks;
recorder->update();
delegate->dispatch();
update();
}

View File

@ -40,7 +40,9 @@ struct Input : Node
{"f9", SDLK_F9},
{"f10", SDLK_F10},
{"f11", SDLK_F11},
{"f12", SDLK_F11}
{"f12", SDLK_F11},
{"enter", SDLK_RETURN},
{"space", SDLK_SPACE}
};
std::vector<KeyCombination> key_map;

View File

@ -15,16 +15,6 @@ nlohmann::json& Node::get_configuration()
return get_root()->configuration->config;
}
Delegate* Node::get_delegate()
{
return get_root()->delegate;
}
Display* Node::get_display()
{
return get_root()->display;
}
Game* Node::get_root()
{
Node *current = this;
@ -35,6 +25,16 @@ Game* Node::get_root()
return static_cast<Game*>(current);
}
Delegate* Node::get_delegate()
{
return get_root()->delegate;
}
Display* Node::get_display()
{
return get_root()->display;
}
void Node::print_branch()
{
Node *current = this;

View File

@ -11,6 +11,7 @@
struct Game;
struct Delegate;
struct Display;
struct TimeFilter;
struct Node
{

View File

@ -11,65 +11,71 @@ void Recorder::respond(SDL_Event& event)
{
capture_screen();
}
else if (get_delegate()->compare(event, "record"))
{
if (animation.playing)
{
end_recording();
}
else
{
start_recording();
}
}
}
void Recorder::capture_screen()
{
nlohmann::json config = get_configuration();
SDL_Surface* surface = get_screen_surface();
SDL_Surface* surface = get_display()->get_screen_surface();
fs::path directory = config["path"]["screenshots"];
fs::create_directories(directory);
std::string prefix = config["display"]["screenshot-prefix"].
get<std::string>();
std::string extension = config["display"]["screenshot-extension"].
get<std::string>();
std::stringstream file_pattern;
file_pattern << prefix << "(.*)" << extension;
fs::path query = directory / file_pattern.str();
std::vector<fs::path> files = sfw::glob(query);
int zfill = config["display"]["screenshot-zfill"].get<int>();
int index = 1;
if (files.size())
{
const std::string last = files.back().string();
std::smatch matches;
std::regex_match(last, matches, std::regex(query.string()));
index = std::stoi(matches[1]) + 1;
}
std::stringstream filename;
fs::path path;
do
{
filename << prefix << sfw::pad(index++, zfill) << extension;
path = directory / filename.str();
filename.str("");
filename.clear();
}
while (fs::exists(path));
fs::path path = sfw::get_next_file_name(directory, zfill, prefix, extension);
IMG_SavePNG(surface, path.c_str());
std::cout << "screenshot saved to " << path.string() << std::endl;
SDL_FreeSurface(surface);
std::cout << "Saved screenshot to " << path.string() << std::endl;
}
SDL_Surface* Recorder::get_screen_surface()
void Recorder::start_recording()
{
glm::ivec2 size = get_display()->get_window_size();
unsigned char* pixels = new unsigned char[24 * size.x * size.y];
get_screen_pixels(pixels, size.x, size.y);
SDL_Surface* surface = zoomSurface(
SDL_CreateRGBSurfaceFrom(
pixels, size.x, size.y, 24, 3 * size.x, 0, 0, 0, 0),
1, -1, SMOOTHING_OFF);
delete[] pixels;
return surface;
std::cout << "Starting recording..." << std::endl;
animation.play();
}
void Recorder::get_screen_pixels(unsigned char* pixels, int w, int h, int x, int y)
void Recorder::add_frame_to_video()
{
GLenum format;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
format = GL_RGB;
#else
format = GL_BGR;
#endif
glReadPixels(x, y, w, h, format, GL_UNSIGNED_BYTE, pixels);
frames.push_back(get_display()->get_screen_surface());
}
void Recorder::end_recording()
{
std::cout << "Ending recording..." << std::endl;
animation.reset();
SDL_Surface* frame;
nlohmann::json config = get_configuration();
fs::path root = config["path"]["video"];
fs::create_directories(root);
fs::path directory = sfw::get_next_file_name(root, 5, "video-");
fs::create_directories(directory);
std::cout << "Writing recording to " << directory << "..." << std::endl;
for (int ii = 0; not frames.empty(); ii++)
{
frame = frames.front();
std::stringstream name;
name << sfw::pad(ii, 5) << ".png";
fs::path path = directory / name.str();
IMG_SavePNG(frame, path.string().c_str());
frames.erase(frames.begin());
SDL_FreeSurface(frame);
}
}
void Recorder::update()
{
animation.update();
}

View File

@ -9,18 +9,10 @@
#define GLM_ENABLE_EXPERIMENTAL
#include "glm/ext.hpp"
#define GL_GLEXT_PROTOTYPES
#define GLEW_STATIC
#include "glew/glew.h"
#include <SDL_image.h>
#include "sdl2-gfx/SDL2_gfxPrimitives.h"
#include "sdl2-gfx/SDL2_rotozoom.h"
#include "json/json.hpp"
#include "filesystem.hpp"
#include "Node.hpp"
#include "Animation.hpp"
#include "Delegate.hpp"
#include "Display.hpp"
#include "extension.hpp"
@ -28,11 +20,17 @@
struct Recorder : Node
{
std::vector<SDL_Surface*> frames;
Animation animation = Animation(&Recorder::add_frame_to_video, this, 100);
Recorder(Node*);
void respond(SDL_Event&);
void capture_screen();
SDL_Surface* get_screen_surface();
void get_screen_pixels(unsigned char*, int, int, int = 0, int = 0);
void start_recording();
void add_frame_to_video();
void end_recording();
void update();
std::string get_class_name() { return "Recorder"; }
};

33
src/Timer.cpp Normal file
View File

@ -0,0 +1,33 @@
#include "Timer.hpp"
Timer::Timer()
{
ticks = SDL_GetTicks();
ticks_previous = ticks;
}
void Timer::toggle()
{
is_timing = not is_timing;
}
void Timer::toggle(bool state)
{
is_timing = state;
}
void Timer::reset()
{
elapsed = 0;
}
void Timer::update()
{
ticks = SDL_GetTicks();
frame_duration = ticks - ticks_previous;
if (is_timing)
{
elapsed += frame_duration;
}
ticks_previous = ticks;
}

21
src/Timer.hpp Normal file
View File

@ -0,0 +1,21 @@
#ifndef Timer_h_
#define Timer_h_
#include "SDL.h"
struct Timer
{
int ticks, ticks_previous, frame_duration = 0, elapsed = 0;
bool is_timing = true;
Timer();
void toggle();
void toggle(bool);
void reset();
void update();
};
#endif

View File

@ -9,7 +9,6 @@ std::vector<fs::path> sfw::glob(fs::path query)
}
std::regex expression(query.string());
std::vector<fs::path> files;
std::cout << basename << " " << query << std::endl;
for (auto& entry: fs::directory_iterator(basename))
{
if (std::regex_match(entry.path().string(), expression))
@ -20,3 +19,31 @@ std::vector<fs::path> sfw::glob(fs::path query)
std::sort(files.begin(), files.end());
return files;
}
fs::path sfw::get_next_file_name(
fs::path directory, int zfill, std::string prefix, std::string extension)
{
std::stringstream file_pattern;
file_pattern << prefix << "([0-9]+)" << extension;
fs::path query = directory / file_pattern.str();
std::vector<fs::path> files = sfw::glob(query);
int index = 1;
if (files.size())
{
const std::string last = files.back().string();
std::smatch matches;
std::regex_match(last, matches, std::regex(query.string()));
index = std::stoi(matches[1]) + 1;
}
std::stringstream filename;
fs::path path;
do
{
filename << prefix << sfw::pad(index++, zfill) << extension;
path = directory / filename.str();
filename.str("");
filename.clear();
}
while (fs::exists(path));
return path;
}

View File

@ -13,6 +13,8 @@
namespace sfw
{
std::vector<fs::path> glob(fs::path);
fs::path get_next_file_name(
fs::path, int = 0, std::string = "", std::string = "");
template<typename T>
std::string pad(T end, int width, char fill = '0')