Files
panopainter/src/app.cpp

907 lines
29 KiB
C++

#include "pch.h"
#include "log.h"
#include "app.h"
#include "node_icon.h"
#include "node_dialog_open.h"
#include "node_progress_bar.h"
#ifdef __APPLE__
#include <Foundation/Foundation.h>
#endif
#ifdef __ANDROID__
void android_async_lock(struct engine* engine);
void android_async_swap(struct engine* engine);
void android_async_unlock(struct engine* engine);
#elif _WIN32
bool async_lock_try();
void async_lock();
void async_swap();
void async_unlock();
void destroy_window();
#endif
App App::I; // singleton
void App::create()
{
width = 1920/2;
height = 1080/2;
}
void App::open_document(std::string path)
{
auto start = path.rfind('/') + 1;
doc_name = path.substr(start, path.length() - start - strlen(".ppi"));
doc_dir = path.substr(0, start - 1);
doc_path = path;
canvas->reset_camera();
layers->clear();
canvas->m_canvas->project_open(path, [this](bool success){
// on complete
if (success)
{
async_start();
title_update();
for (auto& i : canvas->m_canvas->m_order)
{
auto l = layers->add_layer(canvas->m_canvas->m_layers[i].m_name.c_str());
l->m_visibility->set_value(canvas->m_canvas->m_layers[i].m_visible);
}
async_end();
}
else
{
message_box("Open Document Error",
"There was an error opening the document.\n"
"It may be inaccessible or corrupted.");
}
});
ActionManager::clear();
}
bool App::request_close()
{
static bool dialog_already_opened = false;
if (!Canvas::I->m_unsaved)
return true;
if (!dialog_already_opened)
{
async_start();
auto* m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Unsaved document");
m->m_message->set_text("Do you want to close without saving?");
m->btn_ok->m_text->set_text("Yes");
m->btn_ok->on_click = [this](Node*) {
#ifdef _WIN32
destroy_window();
PostQuitMessage(0);
#endif
#ifdef __OSX__
[osx_view close];
#endif
Canvas::I->m_unsaved = false;
};
m->btn_cancel->m_text->set_text("No");
m->btn_cancel->on_click = [this,m](Node*) {
m->destroy();
dialog_already_opened = false;
};
async_redraw();
async_end();
dialog_already_opened = true;
}
return false;
}
void App::clear()
{
glClearColor(.1f, .1f, .1f, 1.f);
glViewport(off_x, off_y, (GLsizei)width, (GLsizei)height);
glClear(GL_COLOR_BUFFER_BIT);
}
void App::initAssets()
{
LOG("initializing assets");
FontManager::init();
LOG("initializing assets loading fonts");
FontManager::load(kFont::Arial_11, "data/fonts/Roboto-Regular.ttf", 17);
FontManager::load(kFont::Arial_30, "data/fonts/Roboto-Regular.ttf", 30);
LOG("initializing assets create sampler");
sampler.create(GL_NEAREST);
sampler_stencil.create(GL_LINEAR, GL_REPEAT);
sampler_linear.create(GL_LINEAR);
m_face_plane.create<1>(2, 2);
//sphere.create<8, 8>(1);
LOG("initializing assets load uvs texture");
LOG("initializing assets completed");
}
void App::initLog()
{
#if defined(__IOS__)
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* docpath = (NSString*)[paths objectAtIndex:0];
data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding];
NSString* recpath = [docpath stringByAppendingString:@"/rec"];
rec_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding];
NSError* recerr = nil;
if (![[NSFileManager defaultManager] createDirectoryAtPath:recpath withIntermediateDirectories:YES attributes:nil error:&recerr])
{
LOG("error creating rec path: %s", [[recerr localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
}
#elif defined(__OSX__)
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSPicturesDirectory, NSUserDomainMask, YES);
NSString* docpath = [(NSString*)[paths objectAtIndex:0] stringByAppendingString:@"/PanoPainter"];
NSError* docerr = nil;
if (![[NSFileManager defaultManager] createDirectoryAtPath:docpath withIntermediateDirectories:YES attributes:nil error:&docerr])
{
LOG("error creating rec path: %s", [[docerr localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
}
data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding];
NSString* recpath = [docpath stringByAppendingString:@"/rec"];
rec_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding];
NSError* recerr = nil;
if (![[NSFileManager defaultManager] createDirectoryAtPath:recpath withIntermediateDirectories:YES attributes:nil error:&recerr])
{
LOG("error creating rec path: %s", [[recerr localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
}
#elif defined(_WIN32)
//CHAR my_documents[MAX_PATH];
//HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
//HMODULE hModule = GetModuleHandle(NULL);
//CHAR path[MAX_PATH];
//GetModuleFileNameA(hModule, path, MAX_PATH);
//CHAR out_drive[MAX_PATH];
//CHAR out_path[MAX_PATH];
//_splitpath(path, out_drive, out_path, nullptr, nullptr);
//sprintf_s(path, "%s%s", out_drive, out_path);
//data_path = path;
CHAR my_documents[MAX_PATH];
HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
if (SUCCEEDED(result))
{
std::string path = std::string(my_documents) + "\\PanoPainter";
if (!PathFileExistsA(path.c_str()))
CreateDirectoryA(path.c_str(), NULL);
data_path = path;
}
else
{
CHAR path[MAX_PATH];
GetCurrentDirectoryA(sizeof(path), path);
data_path = path;
}
rec_path = data_path + "\\frames";
if (!PathFileExistsA(rec_path.c_str()))
CreateDirectoryA(rec_path.c_str(), NULL);
#endif
// TODO: save this path somewhere in the settings, don't overwrite every start
work_path = data_path;
//LogRemote::I.start();
LogRemote::I.file_init();
LOG("%s", g_version);
}
int progress_callback_download(void *clientp, curl_off_t dltotal,
curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
std::function<void(float)> progress = *(std::function<void(float)>*)clientp;
progress((float)dlnow / (float)dltotal);
return 0;
}
int progress_callback_upload(void *clientp, curl_off_t dltotal,
curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
std::function<void(float)> progress = *(std::function<void(float)>*)clientp;
progress((float)ulnow / (float)ultotal);
return 0;
}
void App::download(std::string url, std::string dest_filepath, std::function<void(float)> progress)
{
CURL *curl = curl_easy_init();
if (curl)
{
FILE* fp = fopen(dest_filepath.c_str(), "wb");
LOG("download %s to %s", url.c_str(), dest_filepath.c_str());
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_write);
#ifdef __ANDROID__
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
#endif
if (progress)
{
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback_download);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progress);
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
}
auto err = curl_easy_perform(curl);
curl_easy_cleanup(curl);
fclose(fp);
}
}
bool App::check_license()
{
return true; // TODO: for distribuiton only
CURL *curl = curl_easy_init();
if (curl)
{
std::string url = "https://panopainter.com/license/7565D057-ACBE-4721-9A4E-F342D3DDB7D8.php";
//std::string url = "https://panopainter.com/license/E8EDC2FE-E1BD-4AB1-91BD-FCCD926739BD.php"; // wacom
//std::string url = "https://panopainter.com/license/A744FBA9-BC6C-43C8-BD24-0CCE24B3D985.php"; // others
std::string ret;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
//curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_handler);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2L);
#ifdef __ANDROID__
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
#endif
auto err = curl_easy_perform(curl);
curl_easy_cleanup(curl);
LOG("License check: %s", ret.c_str());
if (err == CURLcode::CURLE_OK && ret == "success")
return true;
}
return false;
}
void App::upload(std::string filename, std::string name, std::function<void(float)> progress)
{
CURL *curl;
struct curl_httppost *formpost = NULL;
struct curl_httppost *lastptr = NULL;
//curl_global_init(CURL_GLOBAL_ALL);
curl_formadd(&formpost,
&lastptr,
CURLFORM_COPYNAME, "fileToUpload",
CURLFORM_FILE, filename.c_str(),
CURLFORM_END);
curl = curl_easy_init();
std::string res;
if (curl)
{
std::string url = "https://panopainter.com/cloud/cloud-upl.php?name=" + name;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_handler);
#ifdef __ANDROID__
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
#endif
if (progress)
{
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback_upload);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progress);
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
}
auto err = curl_easy_perform(curl);
std::cout << "\n\nUPLOAD RESULT\n" << res << "\n\n\n";
curl_easy_cleanup(curl);
}
}
void App::init()
{
#ifdef _WIN32
static CONSOLE_SCREEN_BUFFER_INFO info;
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
// colors: http://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c
glDebugMessageCallback([](GLenum source, GLenum type, GLuint id,
GLenum severity, GLsizei length, const GLchar* message, const void* userParam)
{
static std::map<GLenum, int> colors = {
{ GL_DEBUG_SEVERITY_NOTIFICATION, 8 },
{ GL_DEBUG_SEVERITY_LOW, 8 },
{ GL_DEBUG_SEVERITY_MEDIUM, FOREGROUND_GREEN | FOREGROUND_INTENSITY },
{ GL_DEBUG_SEVERITY_HIGH, FOREGROUND_RED | FOREGROUND_INTENSITY },
};
if (severity == GL_DEBUG_SEVERITY_HIGH || severity == GL_DEBUG_SEVERITY_MEDIUM || severity == GL_DEBUG_SEVERITY_LOW)
{
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colors[severity]);
LOG("OPENGL: %.*s", length, message);
FlushConsoleInputBuffer(GetStdHandle(STD_OUTPUT_HANDLE));
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), info.wAttributes);
#ifdef _WIN32
if (severity == GL_DEBUG_SEVERITY_HIGH)
__debugbreak();
#endif
}
}, nullptr);
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
#endif
LOG("GL version: %s", glGetString(GL_VERSION));
LOG("GL vendor: %s", glGetString(GL_VENDOR));
LOG("GL renderer: %s", glGetString(GL_RENDERER));
GLint n_exts;
glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts);
for (int i = 0; i < n_exts; i++)
{
std::string ext = (const char*)glGetStringi(GL_EXTENSIONS, i);
//if (ext.find("debug") != std::string::npos)
{
LOG("%s", glGetStringi(GL_EXTENSIONS, i));
}
}
LOG("Screen Resolution: %dx%d", (int)width, (int)height);
//zoom = ceilf(width / 2000.f);
//zoom = 2;
glDisable(GL_DEPTH_TEST);
#if defined(_WIN32) || defined(__OSX__)
glEnable(GL_PROGRAM_POINT_SIZE);
glEnable(GL_LINE_SMOOTH);
#endif
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX);
initShaders();
initAssets();
initLayout();
title_update();
if (!check_license())
{
message_box("License", "Could not validate this license, running in demo mode.");
}
//GLfloat width_range[2];
//glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, width_range);
//LOG("GL line range: %f - %f", width_range[0], width_range[1]);
LOG("Screen Size: %f %f", width, height);
GLint fb0;
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fb0);
LOG("Default Framebuffer %d", fb0);
}
void App::async_start()
{
#if __OSX__
[osx_view async_lock];
#elif __IOS__
[ios_view async_lock];
#elif __ANDROID__
android_async_lock(and_engine);
#elif _WIN32
async_lock();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
#endif
}
void App::async_update()
{
#if __IOS__
[ios_view->glview bindDrawable];
#elif _WIN32
glBindFramebuffer(GL_FRAMEBUFFER, 0);
#endif
redraw = true;
clear();
update(0);
#if __OSX__
[osx_view async_swap];
#elif __IOS__
[ios_view->glview bindDrawable];
[ios_view async_swap];
#elif __ANDROID__
android_async_swap(and_engine);
#elif _WIN32
async_swap();
#endif
}
void App::async_redraw()
{
redraw = true;
}
void App::async_end()
{
#if __OSX__
[osx_view async_unlock];
#elif __IOS__
[ios_view async_unlock];
#elif __ANDROID__
android_async_unlock(and_engine);
#elif _WIN32
async_unlock();
#endif
}
void App::update(float dt)
{
static float rec_timer = 0.f;
static std::mutex m;
std::lock_guard<std::mutex> _lock(m);
// update offscreen stuff
if (canvas && canvas->m_canvas)
canvas->m_canvas->stroke_draw();
if (!(redraw || animate))
return;
//glClearColor(.1f, .1f, .1f, 1.f);
//glViewport(0, 0, (GLsizei)width, (GLsizei)height);
//glClear(GL_COLOR_BUFFER_BIT);
//if (!canvas->m_mouse_captured)
{
#if _WIN32 || __OSX__
layout.reload();
#endif
if (auto* main = layout[main_id])
{
main->update(width, height, zoom);
stroke->update_controls();
}
}
static glm::vec4 color_button_normal{.1, .1, .1, 1};
static glm::vec4 color_button_hlight{ 1, .0, .0, 1};
CanvasModePen* mode = (CanvasModePen*)canvas->m_canvas->modes[(int)Canvas::kCanvasMode::Draw][0];
layout[main_id]->find<NodeButtonCustom>("btn-pick")->set_color(
mode->m_picking ? color_button_hlight : color_button_normal);
layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_color(
canvas->m_canvas->m_current_mode == Canvas::kCanvasMode::Draw ? color_button_hlight : color_button_normal);
layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_color(
canvas->m_canvas->m_current_mode == Canvas::kCanvasMode::Erase ? color_button_hlight : color_button_normal);
layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_color(
canvas->m_canvas->m_touch_lock ? color_button_hlight : color_button_normal);
auto observer = [this](Node* n)
{
if (n && n->m_display)
{
auto box = n->m_clip;
Node* p = n->parent;
while (p)
{
float pt = YGNodeLayoutGetPadding(p->y_node, YGEdgeTop);
float pr = YGNodeLayoutGetPadding(p->y_node, YGEdgeRight);
float pb = YGNodeLayoutGetPadding(p->y_node, YGEdgeBottom);
float pl = YGNodeLayoutGetPadding(p->y_node, YGEdgeLeft);
glm::vec2 off_p(pl, pt);
glm::vec2 off_s(pr, pb);
glm::vec4 pclip = { xy(p->m_clip) + off_p, zw(p->m_clip) - off_s - off_p };
box = rect_intersection(box, pclip);
p = p->parent;
}
//auto box = n->m_clip;
//glm::ivec4 c = glm::vec4((int)box.x - 1, (int)(height / zoom - box.y - box.w) - 1, (int)box.z + 2, (int)box.w + 2) * zoom;
glm::ivec4 c = glm::vec4((int)box.x, (int)(height / zoom - box.y - box.w), (int)box.z, (int)box.w) * zoom;
glScissor(c.x + off_x, c.y + off_y, c.z, c.w);
n->draw();
return true;
}
return false;
};
/*
uirtt.bindFramebuffer();
uirtt.clear();
glEnable(GL_SCISSOR_TEST);
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
layout[main_id]->m_children[i]->watch(observer);
//msgbox->watch(observer);
glDisable(GL_SCISSOR_TEST);
uirtt.unbindFramebuffer();
*/
#if __IOS__
[ios_view->glview bindDrawable];
#else
glBindFramebuffer(GL_FRAMEBUFFER, 0);
#endif
//canvas->watch(observer);
glEnable(GL_SCISSOR_TEST);
for (int i = 0; i < layout[main_id]->m_children.size(); i++)
layout[main_id]->m_children[i]->watch(observer);
//msgbox->watch(observer);
glDisable(GL_SCISSOR_TEST);
// glEnable(GL_BLEND);
// sampler.bind(0);
// ShaderManager::use(kShader::Texture);
// ShaderManager::u_int(kShaderUniform::Tex, 0);
// ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-1.f, 1.f, -1.f, 1.f));
// glActiveTexture(GL_TEXTURE0);
// uirtt.bindTexture();
// m_face_plane.draw_fill();
// uirtt.unbindTexture();
// glDisable(GL_BLEND);
if (rec_running)
{
rec_timer += dt;
if (rec_timer > 1.f && canvas->m_canvas->m_dirty_stroke)
{
canvas->m_canvas->m_dirty_stroke = false;
LOG("rec tick");
rec_timer = 0.f;
auto data = new uint8_t[width * height * 4];
#if __IOS__
[ios_view->glview bindDrawable];
#else
glBindFramebuffer(GL_FRAMEBUFFER, 0);
#endif
GLint dfbo, rfbo;
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &rfbo);
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &dfbo);
if (dfbo != rfbo)
LOG("DIFFERENT FB");
glReadBuffer(GL_FRONT);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
{
std::lock_guard<std::mutex> lock(rec_mutex);
rec_frames.emplace_back(data);
rec_cv.notify_all();
}
update_rec_frames();
}
}
redraw = false;
}
void App::terminate()
{
LOG("App::terminate");
TextureManager::invalidate();
ShaderManager::invalidate();
layout.clear_context();
//brushes->clear_context();
layers->clear_context();
color->clear_context();
stroke->clear_context();
rec_stop();
}
void App::update_memory_usage(size_t bytes)
{
if (auto txt = layout[main_id]->find<NodeText>("txt-memory"))
{
static char buffer[128];
sprintf(buffer, "History memory: %.2f Mb", bytes / 1024.f / 1024.f);
txt->set_text(buffer);
layout[main_id]->update();
}
}
void App::update_rec_frames()
{
if (auto txt = layout[main_id]->find<NodeText>("txt-rec"))
{
if (rec_running)
{
static char buffer[128];
sprintf(buffer, "Recorder %d frames", rec_count);
txt->set_text(buffer);
}
else
{
txt->set_text("");
}
layout[main_id]->update();
}
}
int App::res_from_index(int i)
{
return res_map[i];
}
int App::res_to_index(int res)
{
return std::distance(res_map.begin(), std::find(res_map.begin(), res_map.end(), res));
}
std::string App::res_to_string(int res)
{
return res_map_str[res_to_index(res)];
}
void App::rec_clear()
{
rec_stop();
#if defined(__IOS__) || defined(__OSX__)
NSString *path = [NSString stringWithUTF8String:rec_path.c_str()];
NSDirectoryEnumerator* en = [[NSFileManager defaultManager] enumeratorAtPath:path];
NSError* err = nil;
BOOL res;
NSString* file;
while (file = [en nextObject])
{
NSString* file_path = [path stringByAppendingPathComponent:file];
[[NSFileManager defaultManager] removeItemAtPath:file_path error:nil];
NSLog(@"delete: %@", file_path);
}
#endif
rec_count = 0;
update_rec_frames();
}
void App::rec_start()
{
if (!rec_running)
{
#if defined(__IOS__) || defined(__OSX__)
NSString* path = [NSString stringWithUTF8String:rec_path.c_str()];
NSArray *dirFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil];
NSArray *jpgFiles = [dirFiles filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self ENDSWITH '.jpg'"]];
rec_count = (int)[jpgFiles count];
update_rec_frames();
rec_thread = std::thread(&App::rec_loop, this);
#else
rec_count = Asset::list_files(rec_path, false, ".*\\.jpg").size();
update_rec_frames();
rec_thread = std::thread(&App::rec_loop, this);
#endif
}
}
void App::rec_stop()
{
if (rec_running)
{
rec_running = false;
rec_cv.notify_all();
if (rec_thread.joinable())
rec_thread.join();
update_rec_frames();
}
}
void stillImageDataReleaseCallback(void *releaseRefCon, const void *baseAddress)
{
free((void *)baseAddress);
}
void App::rec_export(std::string path)
{
int progress = 0;
int tot = rec_count;
auto pb = layout[main_id]->add_child<NodeProgressBar>();
pb->m_progress->SetWidthP(0);
pb->m_title->set_text("Exporting MP4 movie");
async_update();
#if defined(__IOS__) || defined(__OSX__)
NSString* mov_path = [NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()];
if ([[NSFileManager defaultManager] fileExistsAtPath:mov_path])
{
NSLog(@"remove existing mp4");
[[NSFileManager defaultManager] removeItemAtPath:mov_path error:nil];
}
NSURL* url = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()]];
AVAssetWriter* writer = [AVAssetWriter assetWriterWithURL:url fileType:AVFileTypeMPEG4 error:nil];
writer.shouldOptimizeForNetworkUse = NO;
NSDictionary *videoCompressionSettings = @{
AVVideoCodecKey : AVVideoCodecH264,
AVVideoWidthKey : @(width),
AVVideoHeightKey : @(height),
AVVideoCompressionPropertiesKey : @{ AVVideoAverageBitRateKey : @(8<<20) }
};
if (![writer canApplyOutputSettings:videoCompressionSettings forMediaType:AVMediaTypeVideo])
{
NSLog(@"Couldn't add asset writer video input.");
return;
}
AVAssetWriterInput* input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
outputSettings:videoCompressionSettings
sourceFormatHint:nil];
input.expectsMediaDataInRealTime = YES;
NSDictionary *adaptorDict = @{
(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
(id)kCVPixelBufferWidthKey : @(width),
(id)kCVPixelBufferHeightKey : @(height)
};
AVAssetWriterInputPixelBufferAdaptor* _pixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc]
initWithAssetWriterInput:input
sourcePixelBufferAttributes:adaptorDict];
// Add asset writer input to asset writer
if (![writer canAddInput:input]) {
NSLog(@"Couldn't add input to writer.");
return;
}
[writer addInput:input];
CMTime t;
t.timescale = 30;
t.flags = kCMTimeFlags_Valid;
t.epoch = 0;
t.value = 0;
//[writer startSessionAtSourceTime:t];
CVPixelBufferRef buff = NULL;
uint8_t* data = (uint8_t*)calloc(1, width * height * 4);
CVPixelBufferCreateWithBytes(kCFAllocatorDefault, width, height, kCVPixelFormatType_32RGBA, data,
width * 4, stillImageDataReleaseCallback, nil, nil, &buff);
OSStatus err = CVPixelBufferPoolCreatePixelBuffer(nil, _pixelBufferAdaptor.pixelBufferPool, &buff);
if (writer.status == AVAssetWriterStatusUnknown)
{
// If the asset writer status is unknown, implies writing hasn't started yet, hence start writing with start time as the buffer's presentation timestamp
if ([writer startWriting])
{
[writer startSessionAtSourceTime:t];
}
}
if (writer.status == AVAssetWriterStatusWriting)
{
for (int i = 0; i < tot; i++)
{
// If the asset writer status is writing, append sample buffer to its corresponding asset writer input
if (input.readyForMoreMediaData)
{
char path[256];
snprintf(path, sizeof(path), "%s/%04d.jpg", rec_path.c_str(), i);
NSString* img_path = [NSString stringWithUTF8String:path];
NSLog(@"frame: %@", img_path);
#if __OSX__
NSImage *image = [[NSImage alloc] initWithContentsOfFile:img_path];
if (!image)
break;
NSRect imageRect = NSMakeRect(0, 0, image.size.width, image.size.height);
CGImageRef cgImage = [image CGImageForProposedRect:&imageRect context:NULL hints:nil];
#elif __IOS__
UIImage* image = [UIImage imageNamed:img_path];
if (!image)
break;
CGImageRef cgImage = image.CGImage;
#endif
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
[NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey, nil];
CGSize sz = image.size;
CVPixelBufferRef pxbuffer = NULL;
{
CGImageRef image = cgImage;
CGSize size = sz;
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options, &pxbuffer);
// CVReturn status = CVPixelBufferPoolCreatePixelBuffer(NULL, adaptor.pixelBufferPool, &pxbuffer);
//NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
//NSParameterAssert(pxdata != NULL);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pxdata, size.width, size.height, 8, 4*size.width, rgbColorSpace, kCGImageAlphaPremultipliedFirst);
//NSParameterAssert(context);
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
}
t.value = i;
if (![_pixelBufferAdaptor appendPixelBuffer:pxbuffer withPresentationTime:t])
{
NSLog(@"error %@", [writer.error localizedFailureReason]);
}
CFRelease(pxbuffer);
progress++;
pb->m_progress->SetWidthP((float)progress / tot * 100.f);
async_update();
}
}
[input markAsFinished];
[writer finishWritingWithCompletionHandler:^{
NSString* path = [NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()];
NSLog(@"saved video %@", path);
#if __IOS__
if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(path))
{
NSLog(@"saving to camera roll");
UISaveVideoAtPathToSavedPhotosAlbum(path, nil, nil, nil);
}
#endif
}];
}
if (writer.status == AVAssetWriterStatusFailed)
{
NSLog(@"failed");
}
#endif
pb->destroy();
async_update();
}
void App::rec_loop()
{
rec_running = true;
while(rec_running)
{
std::unique_lock<std::mutex> lock(rec_mutex);
rec_cv.wait(lock);
if (!rec_running)
break;
if (!rec_frames.empty())
{
if (rec_frames.front())
{
auto inverted = std::make_unique<uint8_t[]>(width*height*4);
for (int y = height - 1, y1 = 0; y >= 0; y--, y1++)
{
uint8_t* dst = &inverted[y * width * 4];
uint8_t* src = &rec_frames.front()[y1 * width * 4];
std::copy_n(src, (int)width * 4, dst);
}
char path[256];
snprintf(path, sizeof(path), "%s/%04d.jpg", rec_path.c_str(), rec_count);
LOG("writing %s", path);
jpge::params params;
params.m_quality = 75;
bool saved = jpge::compress_image_to_jpeg_file(path, width, height, 4, inverted.get(), params);
if (!saved)
{
LOG("error writing the frame");
rec_running = false;
}
else
{
rec_count++;
redraw = true;
}
}
rec_frames.pop_front();
}
}
}