#include "Recorder.hpp" #include "gif-h/gif.h" Recorder::Recorder(Node* parent) : Node(parent) { get_delegate().subscribe(&Recorder::respond, this); animation.play(); Mix_SetPostMix(&process_audio, this); if (!get_configuration()["recording"]["enabled"]) { deactivate(); } } float Recorder::get_frame_length() { return get_configuration()["recording"].value( "video-frame-length", get_root()->frame_length); } void Recorder::respond(SDL_Event& event) { if (get_delegate().compare(event, "screenshot")) { capture_screen(); } else if (get_delegate().compare(event, "record")) { if (is_recording) { end_recording(); } else if (!writing_recording) { start_recording(); } else { SDL_Log("Writing in progress, cannot start recording"); } } else if (get_delegate().compare(event, "save-current-stash")) { grab_stash(); } else if (get_delegate().compare(event, "print-video-memory-size")) { SDL_Log("Video memory size is %iMB", get_memory_size()); } } void Recorder::capture_screen() { nlohmann::json config = get_configuration(); SDL_Surface* surface = get_display().get_screen_surface(); fs::path directory = config["path"]["screenshots"]; fs::create_directories(directory); std::string prefix = config["recording"]["screenshot-prefix"]. get(); std::string extension = config["recording"]["screenshot-extension"]. get(); int zfill = config["recording"]["screenshot-zfill"]; fs::path path = sfw::get_next_file_name(directory, zfill, prefix, extension); IMG_SavePNG(surface, path.c_str()); SDL_FreeSurface(surface); SDL_Log("Saved screenshot to %s", path.c_str()); } void Recorder::grab_stash() { if (!is_recording and !writing_recording) { int length = get_configuration()["recording"]["max-stash-length"]; SDL_Log("Stashing most recent %i seconds of video...", length / 1000); most_recent_stash = current_stash; current_stash = Stash(); writing_recording = true; std::function f = std::bind(&Recorder::write_most_recent_frames, this); std::thread writing(f); writing.detach(); } else { SDL_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 (get_configuration()["recording"]["write-mp4"]) { write_mp4(); } SDL_Log("Wrote video frames to %s", current_video_directory.c_str()); writing_recording = false; } void Recorder::start_recording() { if (!writing_recording) { SDL_Log("Starting recording..."); is_recording = true; video_stashes.push_back(Stash()); make_directory(); open_audio_file(); } else { SDL_Log("Writing in progress, cannot start recording"); } } 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 = get_display().get_window_size(); int bytes = Display::bpp / 8 * size.x * size.y; unsigned char* pixels = new unsigned char[bytes]; get_display().get_screen_pixels(pixels, size.x, size.y); int max_length = get_configuration()["recording"]["max-stash-length"]; float length = get_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(get_root()->is_gl_context); 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(get_root()->is_gl_context); if (video_stashes.back().pixel_buffers.size() * get_frame_length() > max_length) { std::function f = std::bind(&Recorder::write_stash_frames, this, std::placeholders::_1); std::thread writing(f, &video_stashes.back()); writing.detach(); // int frame_offset = video_stashes.back().frame_offset; // std::vector pixel_buffers = video_stashes.back().pixel_buffers; // std::vector flipped = video_stashes.back().flipped; // for (int ii = frame_offset; ii < pixel_buffers.size() + frame_offset; ii++) // { // SDL_Surface* frame = get_display().get_screen_surface_from_pixels( // pixel_buffers[ii - frame_offset], flipped[ii - frame_offset]); // std::stringstream name; // name << sfw::pad(ii, 5) << ".png"; // fs::path path = current_video_directory / name.str(); // SDL_Log("%s (%i, %i) (%i, %i, %i, %i)", path.c_str(), frame->w, frame->h, // ((unsigned char*) frame->pixels)[0], ((unsigned char*) frame->pixels)[1], // ((unsigned char*) frame->pixels)[2], ((unsigned char*) frame->pixels)[3]); // IMG_SavePNG(frame, path.string().c_str()); // } // end_recording(); // write_stash_frames(video_stashes.back().pixel_buffers, // video_stashes.back().flipped, // video_stashes.back().frame_offset); video_stashes.push_back( Stash(video_stashes.back().frame_offset + video_stashes.back().pixel_buffers.size())); } } } int Recorder::get_memory_size() { glm::ivec2 window = get_display().get_window_size(); int bytes_per_frame = Display::bpp / 8 * window.x * window.y, size_in_bytes = 0; for (Stash& stash : in_game_stashes) { size_in_bytes += stash.pixel_buffers.size() * bytes_per_frame; for (int& length : stash.audio_buffer_lengths) { size_in_bytes += length; } } for (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 (int& length : current_stash.audio_buffer_lengths) { size_in_bytes += length; } size_in_bytes += most_recent_stash.pixel_buffers.size() * bytes_per_frame; for (int& length : most_recent_stash.audio_buffer_lengths) { size_in_bytes += length; } return size_in_bytes / 1000000; } void Recorder::make_directory() { 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); 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.c_str()); SDL_Surface* frame; GifWriter gif_writer; int gif_frame_length = get_configuration()["recording"]["gif-frame-length"]; fs::path gif_path = sfw::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; not stash->pixel_buffers.empty(); ii++) { frame = get_display().get_screen_surface_from_pixels( stash->pixel_buffers.front(), stash->flipped.front()); std::stringstream name; name << sfw::pad(ii, 5) << ".png"; fs::path path = current_video_directory / name.str(); IMG_SavePNG(frame, path.string().c_str()); if (ii == stash->frame_offset or 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 / 10); } 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 / 10); } elapsed += get_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(); int max_stashes = get_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 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 (get_configuration()["recording"]["write-mp4"]) { write_mp4(); } std::cout << "Wrote video frames to " << current_video_directory.string() << std::endl; writing_recording = false; } void Recorder::write_mp4() { glm::ivec2 size = get_display().get_window_size(); std::stringstream mp4_command; std::string pixel_format = get_configuration()["recording"]["mp4-pixel-format"].get(); fs::path images_match = current_video_directory / "%05d.png"; mp4_command << "ffmpeg -f s16le -ac 2 -ar 22050 -i " << current_audio_path.string() << " -f image2 -framerate " << (1000 / get_frame_length()) << " -i " << images_match.string() << " -s " << size.x << "x" << size.y << " -c:v libx264 -crf 17 -pix_fmt " << pixel_format << " " << current_video_directory.string() << ".mp4"; 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(stream), len); } void Recorder::update() { if (is_recording and get_memory_size() > get_configuration()["recording"]["max-video-memory"]) { end_recording(); } animation.set_frame_length(get_frame_length()); animation.update(); } void process_audio(void* context, Uint8* stream, int len) { Recorder* recorder = static_cast(context); if (recorder->is_active()) { int max_length = recorder->get_configuration()["recording"]["max-stash-length"]; float length = recorder->get_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); } } }