fix export animation frames and add export mp4
This commit is contained in:
@@ -171,6 +171,9 @@
|
|||||||
<button-custom id="file-submenu-export-anim" height="40" align="center" color=".2" pad="0 0 0 10" dir="row">
|
<button-custom id="file-submenu-export-anim" height="40" align="center" color=".2" pad="0 0 0 10" dir="row">
|
||||||
<text text="Animation Frames" grow="1" margin="0 0 0 5"/>
|
<text text="Animation Frames" grow="1" margin="0 0 0 5"/>
|
||||||
</button-custom>
|
</button-custom>
|
||||||
|
<button-custom id="file-submenu-export-anim-mp4" height="40" align="center" color=".2" pad="0 0 0 10" dir="row">
|
||||||
|
<text text="Animation MP4" grow="1" margin="0 0 0 5"/>
|
||||||
|
</button-custom>
|
||||||
<button-custom id="file-submenu-export-timelapse" height="40" align="center" color=".2" pad="0 0 0 10" dir="row">
|
<button-custom id="file-submenu-export-timelapse" height="40" align="center" color=".2" pad="0 0 0 10" dir="row">
|
||||||
<text text="Timelapse" grow="1" margin="0 0 0 5"/>
|
<text text="Timelapse" grow="1" margin="0 0 0 5"/>
|
||||||
</button-custom>
|
</button-custom>
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ public:
|
|||||||
void dialog_browse();
|
void dialog_browse();
|
||||||
void dialog_export(std::string ext);
|
void dialog_export(std::string ext);
|
||||||
void dialog_export_layers();
|
void dialog_export_layers();
|
||||||
|
void dialog_export_anim_frames();
|
||||||
void dialog_export_depth();
|
void dialog_export_depth();
|
||||||
void dialog_export_cube_faces();
|
void dialog_export_cube_faces();
|
||||||
void dialog_layer_rename();
|
void dialog_layer_rename();
|
||||||
|
|||||||
@@ -496,6 +496,36 @@ void App::dialog_export_layers()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::dialog_export_anim_frames()
|
||||||
|
{
|
||||||
|
if (!check_license())
|
||||||
|
{
|
||||||
|
message_box("License", "This function is disabled in demo mode.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canvas)
|
||||||
|
{
|
||||||
|
#if defined(__IOS__)
|
||||||
|
auto dir = work_path + "/" + doc_name + "_frames";
|
||||||
|
if (Asset::create_dir(dir))
|
||||||
|
{
|
||||||
|
auto p = dir + "/" + doc_name;
|
||||||
|
canvas->m_canvas->export_anim_frames(p, [this, p] {
|
||||||
|
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
pick_dir([this](std::string path) {
|
||||||
|
auto p = path + "/" + doc_name;
|
||||||
|
canvas->m_canvas->export_anim_frames(p, [this, p] {
|
||||||
|
message_box("Export Layers", "Layers exported to: " + p);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void App::dialog_export_depth()
|
void App::dialog_export_depth()
|
||||||
{
|
{
|
||||||
if (!check_license())
|
if (!check_license())
|
||||||
@@ -657,7 +687,7 @@ void App::dialog_timelapse_export()
|
|||||||
rec_export(path);
|
rec_export(path);
|
||||||
},
|
},
|
||||||
[this](const std::string& path, bool saved) {
|
[this](const std::string& path, bool saved) {
|
||||||
message_box("Export Timelapse", "Timelapse exported succesfully.");
|
message_box("Export Timelapse", "Timelapse exported successfully.");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
#else
|
#else
|
||||||
@@ -673,174 +703,22 @@ void App::dialog_timelapse_export()
|
|||||||
|
|
||||||
void App::dialog_export_mp4()
|
void App::dialog_export_mp4()
|
||||||
{
|
{
|
||||||
std::thread([this] {
|
#if __IOS__ || __WEB__
|
||||||
BT_SetTerminate();
|
pick_file_save("mp4", doc_name + "-animation",
|
||||||
const int video_width = 1024;
|
[this](std::string path) {
|
||||||
const int video_height = 512;
|
export_anim_mp4(path);
|
||||||
const int video_frames = 500;
|
},
|
||||||
|
[this](const std::string& path, bool saved) {
|
||||||
auto pb = show_progress("Export MP4", video_frames);
|
message_box("Export Animation", "Animation exported successfully.");
|
||||||
|
|
||||||
ISVCEncoder* encoder;
|
|
||||||
int rv = WelsCreateSVCEncoder(&encoder);
|
|
||||||
|
|
||||||
//SEncParamBase param;
|
|
||||||
//memset(¶m, 0, sizeof(SEncParamBase));
|
|
||||||
//param.iUsageType = EUsageType::CAMERA_VIDEO_REAL_TIME; //from EUsageType enum
|
|
||||||
//param.fMaxFrameRate = 25;
|
|
||||||
//param.iPicWidth = 400;
|
|
||||||
//param.iPicHeight = 400;
|
|
||||||
//param.iTargetBitrate = 5000000;
|
|
||||||
//param.iRCMode = RC_TIMESTAMP_MODE;
|
|
||||||
//encoder->Initialize(¶m);
|
|
||||||
|
|
||||||
//Encoder params
|
|
||||||
SEncParamExt param;
|
|
||||||
encoder->GetDefaultParams(¶m);
|
|
||||||
param.iUsageType = CAMERA_VIDEO_REAL_TIME;
|
|
||||||
param.fMaxFrameRate = 25.f;
|
|
||||||
param.iLtrMarkPeriod = 75;
|
|
||||||
param.iPicWidth = 1024;
|
|
||||||
param.iPicHeight = 512;
|
|
||||||
param.iTargetBitrate = 1000 << 10;
|
|
||||||
param.bEnableDenoise = false;
|
|
||||||
param.iSpatialLayerNum = 1;
|
|
||||||
param.bUseLoadBalancing = false;
|
|
||||||
param.bEnableSceneChangeDetect = false;
|
|
||||||
param.bEnableBackgroundDetection = false;
|
|
||||||
param.bEnableAdaptiveQuant = false;
|
|
||||||
param.bEnableFrameSkip = false;
|
|
||||||
param.iMultipleThreadIdc = 0;
|
|
||||||
//param.uiIntraPeriod = 10;
|
|
||||||
|
|
||||||
for (int i = 0; i < param.iSpatialLayerNum; i++)
|
|
||||||
{
|
|
||||||
param.sSpatialLayers[i].iVideoWidth = param.iPicWidth >> (param.iSpatialLayerNum - 1 - i);
|
|
||||||
param.sSpatialLayers[i].iVideoHeight = param.iPicHeight >> (param.iSpatialLayerNum - 1 - i);
|
|
||||||
param.sSpatialLayers[i].fFrameRate = 25.f;
|
|
||||||
param.sSpatialLayers[i].iSpatialBitrate = param.iTargetBitrate;
|
|
||||||
param.sSpatialLayers[i].uiProfileIdc = PRO_BASELINE;
|
|
||||||
param.sSpatialLayers[i].uiLevelIdc = LEVEL_4_2;
|
|
||||||
param.sSpatialLayers[i].iDLayerQp = 42;
|
|
||||||
|
|
||||||
//SSliceArgument sliceArg;
|
|
||||||
//sliceArg.uiSliceMode = SM_FIXEDSLCNUM_SLICE;
|
|
||||||
//sliceArg.uiSliceNum = 0;
|
|
||||||
//param.sSpatialLayers[i].sSliceArgument = sliceArg;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
param.uiMaxNalSize = 1500;
|
#else
|
||||||
param.iTargetBitrate *= param.iSpatialLayerNum;
|
pick_file_save({ "mp4" }, [this](std::string path) {
|
||||||
encoder->InitializeExt(¶m);
|
Canvas::I->export_anim_mp4(path, [this, path] {
|
||||||
|
message_box("Export Animation", "Animation exported to: " + path);
|
||||||
|
});
|
||||||
int trace_level = WELS_LOG_ERROR;
|
});
|
||||||
encoder->SetOption(ENCODER_OPTION_TRACE_LEVEL, &trace_level);
|
#endif
|
||||||
int videoFormat = videoFormatI420;
|
|
||||||
encoder->SetOption(ENCODER_OPTION_DATAFORMAT, &videoFormat);
|
|
||||||
|
|
||||||
int frameSize = param.iPicWidth * param.iPicHeight * 3 / 2;
|
|
||||||
std::vector<uint8_t> buf(frameSize);
|
|
||||||
|
|
||||||
SFrameBSInfo info;
|
|
||||||
memset(&info, 0, sizeof(SFrameBSInfo));
|
|
||||||
SSourcePicture pic;
|
|
||||||
memset(&pic, 0, sizeof(SSourcePicture));
|
|
||||||
pic.iPicWidth = param.iPicWidth;
|
|
||||||
pic.iPicHeight = param.iPicHeight;
|
|
||||||
pic.iColorFormat = videoFormatI420;
|
|
||||||
pic.iStride[0] = pic.iPicWidth;
|
|
||||||
pic.iStride[1] = pic.iStride[2] = pic.iPicWidth >> 1;
|
|
||||||
pic.pData[0] = buf.data();
|
|
||||||
pic.pData[1] = pic.pData[0] + (param.iPicWidth * param.iPicHeight);
|
|
||||||
pic.pData[2] = pic.pData[1] + (param.iPicWidth * param.iPicHeight >> 2);
|
|
||||||
|
|
||||||
std::string mp4_path = data_path + "/out.mp4";
|
|
||||||
|
|
||||||
MP4FileHandle mp4 = MP4Create(mp4_path.c_str());
|
|
||||||
MP4TrackId mp4_track = -1;
|
|
||||||
MP4SetTimeScale(mp4, 90000);
|
|
||||||
|
|
||||||
const int frames = 500;
|
|
||||||
//std::ofstream f("out.h264", std::ios::binary);
|
|
||||||
for (int num = 0; num < frames; num++)
|
|
||||||
{
|
|
||||||
pb->increment();
|
|
||||||
printf("encoding %.2f%%\r", (float)num / (float)(frames - 1) * 100.f);
|
|
||||||
|
|
||||||
float value = sinf((float)num / (float)frames * 10.f);
|
|
||||||
for (int y = 0; y < param.iPicHeight; y++)
|
|
||||||
for (int x = 0; x < param.iPicWidth; x++)
|
|
||||||
buf[y * param.iPicWidth + x] = (((y + num / 4) / 10) % 2) * (255.f * value);
|
|
||||||
|
|
||||||
//prepare input data
|
|
||||||
rv = encoder->EncodeFrame(&pic, &info);
|
|
||||||
//pic.uiTimeStamp += (1.f/25.f) * 1000.f;
|
|
||||||
//assert (rv == cmResultSuccess);
|
|
||||||
if (info.eFrameType != videoFrameTypeSkip)
|
|
||||||
{
|
|
||||||
//output bitstream handling
|
|
||||||
for (int layer = 0; layer < info.iLayerNum; layer++)
|
|
||||||
{
|
|
||||||
size_t bs_size = 0;
|
|
||||||
for (int nal = 0; nal < info.sLayerInfo[layer].iNalCount; nal++)
|
|
||||||
{
|
|
||||||
std::array<uint8_t, 5> nalu_bytes;
|
|
||||||
for (int i = 0; i < nalu_bytes.size(); i++)
|
|
||||||
nalu_bytes[i] = info.sLayerInfo[layer].pBsBuf[bs_size + i];
|
|
||||||
if (nalu_bytes[4] == 0x67) // SPS
|
|
||||||
{
|
|
||||||
uint8_t avc_profile = info.sLayerInfo[layer].pBsBuf[bs_size + 5];
|
|
||||||
uint8_t avc_profile_compat = info.sLayerInfo[layer].pBsBuf[bs_size + 6];
|
|
||||||
uint8_t avc_level = info.sLayerInfo[layer].pBsBuf[bs_size + 7];
|
|
||||||
if (mp4_track == -1)
|
|
||||||
{
|
|
||||||
mp4_track = MP4AddH264VideoTrack(mp4, 90000, 90000 / 25,
|
|
||||||
param.iPicWidth, param.iPicHeight,
|
|
||||||
avc_profile, avc_profile_compat, avc_level, 3);
|
|
||||||
MP4SetVideoProfileLevel(mp4, 1);
|
|
||||||
}
|
|
||||||
MP4AddH264SequenceParameterSet(mp4, mp4_track, info.sLayerInfo[layer].pBsBuf + bs_size + 4,
|
|
||||||
info.sLayerInfo[layer].pNalLengthInByte[nal] - 4);
|
|
||||||
}
|
|
||||||
else if (nalu_bytes[4] == 0x68) // PPS
|
|
||||||
{
|
|
||||||
MP4AddH264PictureParameterSet(mp4, mp4_track, info.sLayerInfo[layer].pBsBuf + bs_size + 4,
|
|
||||||
info.sLayerInfo[layer].pNalLengthInByte[nal] - 4);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
int nalu_sz = info.sLayerInfo[layer].pNalLengthInByte[nal];
|
|
||||||
uint8_t* data = info.sLayerInfo[layer].pBsBuf + bs_size;
|
|
||||||
*(uint32_t*)data = BinaryStream::htonx(nalu_sz - 4);
|
|
||||||
bool sync = false;
|
|
||||||
if (nalu_bytes[4] == 0x65) // I-frame
|
|
||||||
sync = true;
|
|
||||||
else // 0x61 P-frame
|
|
||||||
sync = false;
|
|
||||||
MP4WriteSample(mp4, mp4_track, data, nalu_sz, MP4_INVALID_DURATION, 0, sync);
|
|
||||||
}
|
|
||||||
//printf("nalu %x\n", nalu_bytes[4]);
|
|
||||||
bs_size += info.sLayerInfo[layer].pNalLengthInByte[nal];
|
|
||||||
}
|
|
||||||
//f.write((const char*)info.sLayerInfo[layer].pBsBuf, bs_size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//f.close();
|
|
||||||
|
|
||||||
printf("\n");
|
|
||||||
|
|
||||||
MP4Close(mp4);
|
|
||||||
|
|
||||||
if (encoder)
|
|
||||||
{
|
|
||||||
encoder->Uninitialize();
|
|
||||||
WelsDestroySVCEncoder(encoder);
|
|
||||||
}
|
|
||||||
|
|
||||||
pb->destroy();
|
|
||||||
}).detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::dialog_whatsnew(bool force_show)
|
void App::dialog_whatsnew(bool force_show)
|
||||||
|
|||||||
@@ -654,7 +654,14 @@ void App::init_menu_file()
|
|||||||
subpopup->destroy();
|
subpopup->destroy();
|
||||||
};
|
};
|
||||||
subpopup->find<NodeButtonCustom>("file-submenu-export-anim")->on_click = [this, subpopup, popup](Node*) {
|
subpopup->find<NodeButtonCustom>("file-submenu-export-anim")->on_click = [this, subpopup, popup](Node*) {
|
||||||
dialog_export_layers();
|
dialog_export_anim_frames();
|
||||||
|
popup->mouse_release();
|
||||||
|
popup->destroy();
|
||||||
|
subpopup->mouse_release();
|
||||||
|
subpopup->destroy();
|
||||||
|
};
|
||||||
|
subpopup->find<NodeButtonCustom>("file-submenu-export-anim-mp4")->on_click = [this, subpopup, popup](Node*) {
|
||||||
|
dialog_export_mp4();
|
||||||
popup->mouse_release();
|
popup->mouse_release();
|
||||||
popup->destroy();
|
popup->destroy();
|
||||||
subpopup->mouse_release();
|
subpopup->mouse_release();
|
||||||
|
|||||||
@@ -2023,6 +2023,71 @@ void Canvas::export_layers_thread(std::string path)
|
|||||||
pb->destroy();
|
pb->destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Canvas::export_anim_frames(std::string path, std::function<void()> on_complete)
|
||||||
|
{
|
||||||
|
if (App::I->check_license())
|
||||||
|
{
|
||||||
|
std::thread t([=] {
|
||||||
|
BT_SetTerminate();
|
||||||
|
export_anim_frames_thread(path);
|
||||||
|
if (on_complete)
|
||||||
|
on_complete();
|
||||||
|
});
|
||||||
|
t.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Canvas::export_anim_frames_thread(std::string path)
|
||||||
|
{
|
||||||
|
auto pb = App::I->show_progress("Export Frames", anim_duration());
|
||||||
|
for (int i = 0; i < anim_duration(); i++)
|
||||||
|
{
|
||||||
|
anim_goto_frame(i);
|
||||||
|
export_equirectangular_thread(fmt::format("{}-{:02d}.png", path, i));
|
||||||
|
pb->increment();
|
||||||
|
}
|
||||||
|
pb->destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Canvas::export_anim_mp4(std::string path, std::function<void()> on_complete)
|
||||||
|
{
|
||||||
|
if (App::I->check_license())
|
||||||
|
{
|
||||||
|
std::thread t([=] {
|
||||||
|
BT_SetTerminate();
|
||||||
|
export_anim_mp4_thread(path);
|
||||||
|
if (on_complete)
|
||||||
|
on_complete();
|
||||||
|
});
|
||||||
|
t.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Canvas::export_anim_mp4_thread(std::string path)
|
||||||
|
{
|
||||||
|
auto pb = App::I->show_progress("Export Animation", anim_duration());
|
||||||
|
int fps = App::I->animation->get_fps();
|
||||||
|
MP4Encoder mp4;
|
||||||
|
int res = std::min<int>(1024, m_width);
|
||||||
|
mp4.init(res * 4, res * 2, 30, 2 << 20);
|
||||||
|
for (int i = 0; i < anim_duration(); i++)
|
||||||
|
{
|
||||||
|
Image data;
|
||||||
|
App::I->render_task([&]
|
||||||
|
{
|
||||||
|
anim_goto_frame(i);
|
||||||
|
draw_merge(false);
|
||||||
|
Texture2D equirect = m_layers_merge.gen_equirect({ res, res });
|
||||||
|
data = equirect.get_image();
|
||||||
|
});
|
||||||
|
for (int j = 0; j < 30/fps; j++)
|
||||||
|
mp4.encode(data);
|
||||||
|
pb->increment();
|
||||||
|
}
|
||||||
|
mp4.write_mp4(path);
|
||||||
|
pb->destroy();
|
||||||
|
}
|
||||||
|
|
||||||
void Canvas::export_cube_faces(std::string file_name, std::function<void()> on_complete)
|
void Canvas::export_cube_faces(std::string file_name, std::function<void()> on_complete)
|
||||||
{
|
{
|
||||||
if (App::I->check_license())
|
if (App::I->check_license())
|
||||||
|
|||||||
@@ -229,6 +229,10 @@ public:
|
|||||||
void export_equirectangular_thread(std::string file_path);
|
void export_equirectangular_thread(std::string file_path);
|
||||||
void export_layers(std::string path, std::function<void()> on_complete = nullptr);
|
void export_layers(std::string path, std::function<void()> on_complete = nullptr);
|
||||||
void export_layers_thread(std::string path);
|
void export_layers_thread(std::string path);
|
||||||
|
void export_anim_frames(std::string path, std::function<void()> on_complete = nullptr);
|
||||||
|
void export_anim_frames_thread(std::string path);
|
||||||
|
void export_anim_mp4(std::string path, std::function<void()> on_complete = nullptr);
|
||||||
|
void export_anim_mp4_thread(std::string path);
|
||||||
void export_depth(std::string file_name, std::function<void()> on_complete = nullptr);
|
void export_depth(std::string file_name, std::function<void()> on_complete = nullptr);
|
||||||
void export_depth_thread(std::string file_name);
|
void export_depth_thread(std::string file_name);
|
||||||
void export_cube_faces(std::string file_name, std::function<void()> on_complete);
|
void export_cube_faces(std::string file_name, std::function<void()> on_complete);
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ public:
|
|||||||
void load_layers();
|
void load_layers();
|
||||||
void update_frames();
|
void update_frames();
|
||||||
int get_onion_size() const noexcept { return m_onion->get_int(); }
|
int get_onion_size() const noexcept { return m_onion->get_int(); }
|
||||||
|
int get_fps() const noexcept { return m_fps->get_int(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
Reference in New Issue
Block a user