366 lines
13 KiB
C++
366 lines
13 KiB
C++
/* ✨ +------------------------------------------------------+
|
|
____/ \____ ✨/| Open source game framework licensed to freely use, |
|
|
✨\ / / | copy, and modify. Created for 🌠dank.game🌠 |
|
|
+--\ . . /--+ | |
|
|
| ~/ ︶ \👍| | 🌐 https://open.shampoo.ooo/shampoo/spacebox |
|
|
| ~~~🌊~~~~🌊~ | +------------------------------------------------------+
|
|
| SPACE 🪐🅱 OX | /
|
|
| 🌊 ~ ~~~~ ~~ |/
|
|
+-------------*/
|
|
|
|
#include "gif-h/gif.h"
|
|
#include "Game.hpp"
|
|
#include "extension.hpp"
|
|
#include "Recorder.hpp"
|
|
|
|
using namespace sb;
|
|
|
|
/* Create a Recorder instance. Subscribe to command input and set audio callback. */
|
|
Recorder::Recorder(sb::Configuration& configuration, sb::Display& display) : configuration(configuration), display(display)
|
|
{
|
|
Mix_SetPostMix(Recorder::process_audio, this);
|
|
}
|
|
|
|
void Recorder::toggle()
|
|
{
|
|
if (is_recording)
|
|
{
|
|
end_recording();
|
|
}
|
|
else if (!writing_recording)
|
|
{
|
|
start_recording();
|
|
}
|
|
else
|
|
{
|
|
sb::Log::log("Writing in progress, cannot start recording", sb::Log::WARN);
|
|
}
|
|
}
|
|
|
|
/* Returns length of a recorded video frame in seconds */
|
|
float Recorder::frame_length()
|
|
{
|
|
return configuration("recording", "video frame length");
|
|
}
|
|
|
|
void Recorder::capture_screen()
|
|
{
|
|
SDL_Surface* surface = display.screen_surface();
|
|
fs::path directory = configuration("recording", "screenshot directory").get<std::string>();
|
|
fs::create_directories(directory);
|
|
std::string prefix = configuration("recording", "screenshot prefix").get<std::string>();
|
|
std::string extension = configuration("recording", "screenshot extension").get<std::string>();
|
|
int zfill = configuration("recording", "screenshot zfill");
|
|
fs::path path = sb::get_next_file_name(directory, zfill, prefix, extension);
|
|
IMG_SavePNG(surface, path.string().c_str());
|
|
SDL_FreeSurface(surface);
|
|
std::ostringstream message;
|
|
message << "saved screenshot to " << path;
|
|
sb::Log::log(message);
|
|
}
|
|
|
|
/* Writes a video of what was just displayed on the screen up until the function was called. The length
|
|
* of the video is determined by the stash length written in the configuration. This is accomplished by
|
|
* writing the contents of the most recent Stash object. */
|
|
void Recorder::grab_stash()
|
|
{
|
|
if (!is_recording and !writing_recording)
|
|
{
|
|
int length = configuration("recording", "max stash length");
|
|
std::ostringstream message;
|
|
message << "stashing most recent " << length << " seconds of video";
|
|
sb::Log::log(message);
|
|
most_recent_stash = current_stash;
|
|
current_stash = Stash();
|
|
writing_recording = true;
|
|
std::function<void()> f = std::bind(&Recorder::write_most_recent_frames, this);
|
|
std::thread writing {f};
|
|
writing.detach();
|
|
}
|
|
else
|
|
{
|
|
sb::Log::log("recording in progress, cannot grab most recent frames");
|
|
}
|
|
}
|
|
|
|
void Recorder::write_most_recent_frames()
|
|
{
|
|
make_directory();
|
|
write_stash_frames(&most_recent_stash);
|
|
open_audio_file();
|
|
while (!most_recent_stash.audio_buffers.empty())
|
|
{
|
|
write_audio(most_recent_stash.audio_buffers.front(), most_recent_stash.audio_buffer_lengths.front());
|
|
most_recent_stash.audio_buffers.erase(most_recent_stash.audio_buffers.begin());
|
|
most_recent_stash.audio_buffer_lengths.erase(most_recent_stash.audio_buffer_lengths.begin());
|
|
}
|
|
audio_file.close();
|
|
if (configuration("recording", "write mp4"))
|
|
{
|
|
write_mp4();
|
|
}
|
|
std::ostringstream message;
|
|
message << "wrote video frames to " << current_video_directory;
|
|
sb::Log::log(message);
|
|
writing_recording = false;
|
|
}
|
|
|
|
void Recorder::start_recording()
|
|
{
|
|
if (!writing_recording)
|
|
{
|
|
sb::Log::log("starting recording");
|
|
is_recording = true;
|
|
video_stashes.push_back(Stash());
|
|
make_directory();
|
|
open_audio_file();
|
|
}
|
|
else
|
|
{
|
|
sb::Log::log("writing in progress, cannot start recording", sb::Log::WARN);
|
|
}
|
|
}
|
|
|
|
void Recorder::open_audio_file()
|
|
{
|
|
std::stringstream audio_path;
|
|
audio_path << current_video_directory.string() << ".raw";
|
|
current_audio_path = audio_path.str();
|
|
audio_file.open(audio_path.str(), std::ios::binary);
|
|
}
|
|
|
|
void Recorder::add_frame()
|
|
{
|
|
glm::ivec2 size = display.window_size();
|
|
int bytes = sb::Display::bpp / 8 * size.x * size.y;
|
|
unsigned char* pixels = new unsigned char[bytes];
|
|
display.screen_pixels(pixels, size.x, size.y);
|
|
int max_length = configuration("recording", "max stash length");
|
|
float length = frame_length() * current_stash.pixel_buffers.size();
|
|
if (length > max_length)
|
|
{
|
|
delete[] current_stash.pixel_buffers.front();
|
|
current_stash.pixel_buffers.erase(current_stash.pixel_buffers.begin());
|
|
current_stash.flipped.erase(current_stash.flipped.begin());
|
|
}
|
|
current_stash.pixel_buffers.push_back(pixels);
|
|
current_stash.flipped.push_back(true);
|
|
if (is_recording)
|
|
{
|
|
unsigned char* vid_pixels = new unsigned char[bytes];
|
|
memcpy(vid_pixels, pixels, bytes);
|
|
video_stashes.back().pixel_buffers.push_back(vid_pixels);
|
|
video_stashes.back().flipped.push_back(true);
|
|
if (video_stashes.back().pixel_buffers.size() * frame_length() > max_length)
|
|
{
|
|
std::function<void(Stash*)> f = std::bind(&Recorder::write_stash_frames, this, std::placeholders::_1);
|
|
std::thread writing(f, &video_stashes.back());
|
|
writing.detach();
|
|
video_stashes.push_back(Stash(video_stashes.back().frame_offset + video_stashes.back().pixel_buffers.size()));
|
|
}
|
|
}
|
|
}
|
|
|
|
float Recorder::get_memory_size() const
|
|
{
|
|
glm::ivec2 window = display.window_size();
|
|
int bytes_per_frame = sb::Display::bpp / 8 * window.x * window.y;
|
|
int size_in_bytes = 0;
|
|
for (const Stash& stash : in_game_stashes)
|
|
{
|
|
size_in_bytes += stash.pixel_buffers.size() * bytes_per_frame;
|
|
for (const int& length : stash.audio_buffer_lengths)
|
|
{
|
|
size_in_bytes += length;
|
|
}
|
|
}
|
|
for (const Stash& stash : video_stashes)
|
|
{
|
|
size_in_bytes += stash.pixel_buffers.size() * bytes_per_frame;
|
|
}
|
|
size_in_bytes += current_stash.pixel_buffers.size() * bytes_per_frame;
|
|
for (const int& length : current_stash.audio_buffer_lengths)
|
|
{
|
|
size_in_bytes += length;
|
|
}
|
|
size_in_bytes += most_recent_stash.pixel_buffers.size() * bytes_per_frame;
|
|
for (const int& length : most_recent_stash.audio_buffer_lengths)
|
|
{
|
|
size_in_bytes += length;
|
|
}
|
|
return size_in_bytes / 1'000'000.0;
|
|
}
|
|
|
|
void Recorder::log_video_memory_size() const
|
|
{
|
|
std::ostringstream message;
|
|
message << "Video memory size is " << std::fixed << std::setprecision(2) << get_memory_size() << "MB";
|
|
sb::Log::log(message);
|
|
}
|
|
|
|
void Recorder::make_directory()
|
|
{
|
|
fs::path root = configuration("recording", "video directory").get<std::string>();
|
|
fs::create_directories(root);
|
|
fs::path directory = sb::get_next_file_name(root, 5, "video-");
|
|
fs::create_directories(directory);
|
|
current_video_directory = directory;
|
|
}
|
|
|
|
void Recorder::write_stash_frames(Stash* stash)
|
|
{
|
|
SDL_Log("Writing stash offset %i to %s...", stash->frame_offset, current_video_directory.string().c_str());
|
|
SDL_Surface* frame;
|
|
GifWriter gif_writer;
|
|
float gif_frame_length = configuration("recording", "gif frame length");
|
|
fs::path gif_path = sb::get_next_file_name(current_video_directory, 3, "gif-", ".gif");
|
|
float elapsed = 0, last_gif_write = 0, gif_write_overflow = 0;
|
|
for (int ii = stash->frame_offset; !stash->pixel_buffers.empty(); ii++)
|
|
{
|
|
frame = display.screen_surface_from_pixels(stash->pixel_buffers.front(), stash->flipped.front());
|
|
std::stringstream name;
|
|
name << sb::pad(ii, 5) << ".png";
|
|
fs::path path = current_video_directory / name.str();
|
|
IMG_SavePNG(frame, path.string().c_str());
|
|
if (ii == stash->frame_offset || elapsed - last_gif_write + gif_write_overflow >= gif_frame_length)
|
|
{
|
|
if (ii == stash->frame_offset)
|
|
{
|
|
GifBegin(&gif_writer, gif_path.string().c_str(), frame->w, frame->h, gif_frame_length * 100);
|
|
}
|
|
else
|
|
{
|
|
gif_write_overflow += elapsed - (last_gif_write + gif_frame_length);
|
|
last_gif_write = elapsed;
|
|
}
|
|
SDL_Surface* converted = SDL_ConvertSurfaceFormat(frame, SDL_PIXELFORMAT_ABGR8888, 0);
|
|
GifWriteFrame(&gif_writer, (const uint8_t*) converted->pixels, frame->w, frame->h, gif_frame_length * 100);
|
|
}
|
|
elapsed += frame_length();
|
|
delete[] stash->pixel_buffers.front();
|
|
stash->pixel_buffers.erase(stash->pixel_buffers.begin());
|
|
stash->flipped.erase(stash->flipped.begin());
|
|
SDL_FreeSurface(frame);
|
|
}
|
|
GifEnd(&gif_writer);
|
|
}
|
|
|
|
void Recorder::keep_stash()
|
|
{
|
|
in_game_stashes.push_back(current_stash);
|
|
current_stash = Stash();
|
|
auto max_stashes = configuration("recording", "max in game stashes");
|
|
if (in_game_stashes.size() > max_stashes)
|
|
{
|
|
Stash& stash = in_game_stashes.front();
|
|
while (not stash.pixel_buffers.empty())
|
|
{
|
|
delete[] stash.pixel_buffers.back();
|
|
stash.pixel_buffers.pop_back();
|
|
stash.flipped.pop_back();
|
|
}
|
|
in_game_stashes.erase(in_game_stashes.begin());
|
|
}
|
|
}
|
|
|
|
void Recorder::end_recording()
|
|
{
|
|
std::cout << "Ending recording..." << std::endl;
|
|
audio_file.close();
|
|
is_recording = false;
|
|
writing_recording = true;
|
|
std::function<void()> f = std::bind(&Recorder::finish_writing_video, this);
|
|
std::thread finishing(f);
|
|
finishing.detach();
|
|
}
|
|
|
|
void Recorder::finish_writing_video()
|
|
{
|
|
write_stash_frames(&video_stashes.back());
|
|
int count;
|
|
while (true)
|
|
{
|
|
count = 0;
|
|
for (Stash& stash : video_stashes)
|
|
{
|
|
count += stash.pixel_buffers.size();
|
|
}
|
|
if (count == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
video_stashes.clear();
|
|
if (configuration("recording", "write mp4"))
|
|
{
|
|
write_mp4();
|
|
}
|
|
std::cout << "Wrote video frames to " << current_video_directory.string() << std::endl;
|
|
writing_recording = false;
|
|
}
|
|
|
|
/* Launch a system command that calls ffmpeg to write an x264 encoded MP4 from the image frames written.
|
|
* This requires ffmpeg to be installed on the user's system. Might only work on Linux (?) */
|
|
void Recorder::write_mp4()
|
|
{
|
|
glm::ivec2 size = display.window_size();
|
|
std::ostringstream mp4_command;
|
|
std::string pixel_format = configuration("recording", "mp4 pixel format").get<std::string>();
|
|
int audio_frequency;
|
|
Mix_QuerySpec(&audio_frequency, nullptr, nullptr);
|
|
fs::path images_match = current_video_directory / "%05d.png";
|
|
int frame_count = 0;
|
|
for (auto& p: fs::directory_iterator(current_video_directory))
|
|
{
|
|
frame_count++;
|
|
}
|
|
float video_length = frame_count * frame_length();
|
|
mp4_command << "ffmpeg -f s16le -ac 2 -ar " << audio_frequency << " -i " << current_audio_path.string() <<
|
|
" -f image2 -framerate " << (1.0f / frame_length()) <<
|
|
" -i " << images_match.string() << " -s " << size.x << "x" << size.y << " -t " << video_length <<
|
|
" -c:v libx264 -crf 17 -pix_fmt " << pixel_format << " " <<
|
|
current_video_directory.string() << ".mp4";
|
|
std::string mp4_command_str = mp4_command.str();
|
|
std::cout << mp4_command_str << std::endl;
|
|
std::system(mp4_command_str.c_str());
|
|
}
|
|
|
|
void Recorder::write_audio(Uint8* stream, int len)
|
|
{
|
|
audio_file.write(reinterpret_cast<char*>(stream), len);
|
|
}
|
|
|
|
void Recorder::update(float timestamp)
|
|
{
|
|
if (is_recording && get_memory_size() > configuration("recording", "max video memory"))
|
|
{
|
|
end_recording();
|
|
}
|
|
animation.frame_length(frame_length());
|
|
animation.update(timestamp);
|
|
}
|
|
|
|
void Recorder::process_audio(void* context, Uint8* stream, int len)
|
|
{
|
|
Recorder* recorder = static_cast<Recorder*>(context);
|
|
if (recorder->configuration("recording", "enabled"))
|
|
{
|
|
int max_length = recorder->configuration("recording", "max stash length");
|
|
float length = recorder->frame_length() * recorder->current_stash.pixel_buffers.size();
|
|
if (length > max_length)
|
|
{
|
|
delete[] recorder->current_stash.audio_buffers.front();
|
|
recorder->current_stash.audio_buffers.erase(recorder->current_stash.audio_buffers.begin());
|
|
recorder->current_stash.audio_buffer_lengths.erase(recorder->current_stash.audio_buffer_lengths.begin());
|
|
}
|
|
Uint8* stream_copy = new Uint8[len];
|
|
std::memcpy(stream_copy, stream, len);
|
|
recorder->current_stash.audio_buffers.push_back(stream_copy);
|
|
recorder->current_stash.audio_buffer_lengths.push_back(len);
|
|
if (recorder->is_recording)
|
|
{
|
|
recorder->write_audio(stream_copy, len);
|
|
}
|
|
}
|
|
}
|