diff --git a/data/layout.xml b/data/layout.xml
index f43d180..b1015e3 100644
--- a/data/layout.xml
+++ b/data/layout.xml
@@ -171,6 +171,9 @@
+
diff --git a/src/app.h b/src/app.h
index d8fb0cd..8d3a90b 100644
--- a/src/app.h
+++ b/src/app.h
@@ -254,6 +254,7 @@ public:
void dialog_browse();
void dialog_export(std::string ext);
void dialog_export_layers();
+ void dialog_export_anim_frames();
void dialog_export_depth();
void dialog_export_cube_faces();
void dialog_layer_rename();
diff --git a/src/app_dialogs.cpp b/src/app_dialogs.cpp
index 263ae03..39c980b 100644
--- a/src/app_dialogs.cpp
+++ b/src/app_dialogs.cpp
@@ -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()
{
if (!check_license())
@@ -657,7 +687,7 @@ void App::dialog_timelapse_export()
rec_export(path);
},
[this](const std::string& path, bool saved) {
- message_box("Export Timelapse", "Timelapse exported succesfully.");
+ message_box("Export Timelapse", "Timelapse exported successfully.");
}
);
#else
@@ -673,174 +703,22 @@ void App::dialog_timelapse_export()
void App::dialog_export_mp4()
{
- std::thread([this] {
- BT_SetTerminate();
- const int video_width = 1024;
- const int video_height = 512;
- const int video_frames = 500;
-
- auto pb = show_progress("Export MP4", video_frames);
-
- 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;
+#if __IOS__ || __WEB__
+ pick_file_save("mp4", doc_name + "-animation",
+ [this](std::string path) {
+ export_anim_mp4(path);
+ },
+ [this](const std::string& path, bool saved) {
+ message_box("Export Animation", "Animation exported successfully.");
}
-
- param.uiMaxNalSize = 1500;
- param.iTargetBitrate *= param.iSpatialLayerNum;
- encoder->InitializeExt(¶m);
-
-
- int trace_level = WELS_LOG_ERROR;
- encoder->SetOption(ENCODER_OPTION_TRACE_LEVEL, &trace_level);
- int videoFormat = videoFormatI420;
- encoder->SetOption(ENCODER_OPTION_DATAFORMAT, &videoFormat);
-
- int frameSize = param.iPicWidth * param.iPicHeight * 3 / 2;
- std::vector 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 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();
+ );
+#else
+ pick_file_save({ "mp4" }, [this](std::string path) {
+ Canvas::I->export_anim_mp4(path, [this, path] {
+ message_box("Export Animation", "Animation exported to: " + path);
+ });
+ });
+#endif
}
void App::dialog_whatsnew(bool force_show)
diff --git a/src/app_layout.cpp b/src/app_layout.cpp
index 859e971..8840278 100644
--- a/src/app_layout.cpp
+++ b/src/app_layout.cpp
@@ -654,7 +654,14 @@ void App::init_menu_file()
subpopup->destroy();
};
subpopup->find("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("file-submenu-export-anim-mp4")->on_click = [this, subpopup, popup](Node*) {
+ dialog_export_mp4();
popup->mouse_release();
popup->destroy();
subpopup->mouse_release();
diff --git a/src/canvas.cpp b/src/canvas.cpp
index 34c4bdf..6804bf5 100644
--- a/src/canvas.cpp
+++ b/src/canvas.cpp
@@ -2023,6 +2023,71 @@ void Canvas::export_layers_thread(std::string path)
pb->destroy();
}
+void Canvas::export_anim_frames(std::string path, std::function 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 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(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 on_complete)
{
if (App::I->check_license())
@@ -2032,7 +2097,7 @@ void Canvas::export_cube_faces(std::string file_name, std::function on_c
export_cube_faces_thread(file_name);
if (on_complete)
on_complete();
- });
+ });
t.detach();
}
}
diff --git a/src/canvas.h b/src/canvas.h
index 251fb41..a79d350 100644
--- a/src/canvas.h
+++ b/src/canvas.h
@@ -229,6 +229,10 @@ public:
void export_equirectangular_thread(std::string file_path);
void export_layers(std::string path, std::function on_complete = nullptr);
void export_layers_thread(std::string path);
+ void export_anim_frames(std::string path, std::function on_complete = nullptr);
+ void export_anim_frames_thread(std::string path);
+ void export_anim_mp4(std::string path, std::function on_complete = nullptr);
+ void export_anim_mp4_thread(std::string path);
void export_depth(std::string file_name, std::function on_complete = nullptr);
void export_depth_thread(std::string file_name);
void export_cube_faces(std::string file_name, std::function on_complete);
diff --git a/src/node_panel_animation.h b/src/node_panel_animation.h
index aa5cd9b..8ba671b 100644
--- a/src/node_panel_animation.h
+++ b/src/node_panel_animation.h
@@ -79,6 +79,7 @@ public:
void load_layers();
void update_frames();
int get_onion_size() const noexcept { return m_onion->get_int(); }
+ int get_fps() const noexcept { return m_fps->get_int(); }
};
//////////////////////////////////////////////////////////////////////////