diff --git a/binaries/data/tools/atlas/buttons/next_s.bmp b/binaries/data/tools/atlas/buttons/next_s.bmp new file mode 100644 index 0000000000..db2018414f Binary files /dev/null and b/binaries/data/tools/atlas/buttons/next_s.bmp differ diff --git a/binaries/data/tools/atlas/buttons/pause_s.bmp b/binaries/data/tools/atlas/buttons/pause_s.bmp new file mode 100644 index 0000000000..aed9442dd5 Binary files /dev/null and b/binaries/data/tools/atlas/buttons/pause_s.bmp differ diff --git a/binaries/data/tools/atlas/buttons/play_s.bmp b/binaries/data/tools/atlas/buttons/play_s.bmp new file mode 100644 index 0000000000..5329ea25bc Binary files /dev/null and b/binaries/data/tools/atlas/buttons/play_s.bmp differ diff --git a/binaries/data/tools/atlas/buttons/previous_s.bmp b/binaries/data/tools/atlas/buttons/previous_s.bmp new file mode 100644 index 0000000000..8315185539 Binary files /dev/null and b/binaries/data/tools/atlas/buttons/previous_s.bmp differ diff --git a/binaries/data/tools/atlas/buttons/record_s.bmp b/binaries/data/tools/atlas/buttons/record_s.bmp new file mode 100644 index 0000000000..6307c6665e Binary files /dev/null and b/binaries/data/tools/atlas/buttons/record_s.bmp differ diff --git a/binaries/data/tools/atlas/buttons/stop_s.bmp b/binaries/data/tools/atlas/buttons/stop_s.bmp new file mode 100644 index 0000000000..c027fec5ff Binary files /dev/null and b/binaries/data/tools/atlas/buttons/stop_s.bmp differ diff --git a/build/premake/extern_libs.lua b/build/premake/extern_libs.lua index 72527d6b51..108779a1dc 100644 --- a/build/premake/extern_libs.lua +++ b/build/premake/extern_libs.lua @@ -62,6 +62,10 @@ extern_lib_defs = { win_names = { "ddraw", "dsound" }, dbg_suffix = "", }, + ffmpeg = { + win_names = { "avcodec-51", "avformat-51", "avutil-49" }, + dbg_suffix = "", + }, libjpg = { win_names = { "jpeg-6b" }, unix_names = { "jpeg" }, @@ -95,8 +99,9 @@ extern_lib_defs = { tinsert(package.includepaths, libraries_dir.."wxwidgets/include/msvc") tinsert(package.includepaths, libraries_dir.."wxwidgets/include") tinsert(package.libpaths, libraries_dir.."wxwidgets/lib/vc_lib") - package.config["Debug" ].links = { "wxmsw26ud_gl" } - package.config["Release"].links = { "wxmsw26u_gl" } + tinsert(package.config["Debug" ].links, "wxmsw26ud_gl") + tinsert(package.config["Testing"].links, "wxmsw26ud_gl") + tinsert(package.config["Release"].links, "wxmsw26u_gl") else tinsert(package.buildoptions, "`wx-config --cxxflags`") tinsert(package.linkoptions, "`wx-config --libs std,gl,ogl,media`") diff --git a/build/premake/premake.lua b/build/premake/premake.lua index 6e1b0cf152..9fbb9bfb90 100755 --- a/build/premake/premake.lua +++ b/build/premake/premake.lua @@ -477,7 +477,7 @@ local function setup_atlas_package(package_name, target_type, rel_source_dirs, r tinsert(package.defines, "_UNICODE") -- Link to required libraries - package.links = { "winmm", "comctl32", "rpcrt4" } + package.links = { "winmm", "comctl32", "rpcrt4", "delayimp" } -- required to use WinMain() on Windows, otherwise will default to main() tinsert(package.buildflags, "no-main") @@ -525,6 +525,7 @@ function setup_atlas_packages() "CustomControls/Windows", "FileConverter", "General", + "General/VideoRecorder", "Misc", "ScenarioEditor", "ScenarioEditor/Sections/Common", @@ -543,8 +544,9 @@ function setup_atlas_packages() },{ -- extern_libs "boost", "devil", - "xerces", - "wxwidgets" + "ffmpeg", + "wxwidgets", + "xerces" },{ -- extra_params pch = 1, extra_links = { "AtlasObject", "DatafileIO" }, diff --git a/source/ps/Util.cpp b/source/ps/Util.cpp index bf13fccbff..7abca7bc6a 100644 --- a/source/ps/Util.cpp +++ b/source/ps/Util.cpp @@ -247,7 +247,7 @@ void WriteBigScreenshot(const char* extension, int tiles) g_Renderer.Resize(tile_w, tile_h); SViewPort vp = { 0, 0, tile_w, tile_h }; g_Game->GetView()->GetCamera()->SetViewPort(&vp); - g_Game->GetView()->GetCamera()->SetProjection (CGameView::defaultNear, CGameView::defaultFar, CGameView::defaultFOV); + g_Game->GetView()->GetCamera()->SetProjection(CGameView::defaultNear, CGameView::defaultFar, CGameView::defaultFOV); } // Temporarily move everything onto the front buffer, so the user can @@ -290,7 +290,7 @@ void WriteBigScreenshot(const char* extension, int tiles) g_Renderer.Resize(g_xres, g_yres); SViewPort vp = { 0, 0, g_xres, g_yres }; g_Game->GetView()->GetCamera()->SetViewPort(&vp); - g_Game->GetView()->GetCamera()->SetProjection (1, 5000, DEGTORAD(20)); + g_Game->GetView()->GetCamera()->SetProjection(CGameView::defaultNear, CGameView::defaultFar, CGameView::defaultFOV); g_Game->GetView()->GetCamera()->SetProjectionTile(1, 0, 0); } diff --git a/source/tools/atlas/AtlasUI/General/VideoRecorder/FFmpeg.cpp b/source/tools/atlas/AtlasUI/General/VideoRecorder/FFmpeg.cpp new file mode 100644 index 0000000000..492280710c --- /dev/null +++ b/source/tools/atlas/AtlasUI/General/VideoRecorder/FFmpeg.cpp @@ -0,0 +1,391 @@ +#include "stdafx.h" + +/* + +Code originally taken from ffmpeg's output_example.c +This is all rather hacked together and unreliable. In particular, I should: +* Change the fprintf/exit error handling so that it works in Atlas. +* Send all the logging output to wx too. +* Support variable bitrate (set qscale to 1 (best) .. 31 (worst), sameq) +* See if other codecs (particularly lossless ones) could be made to work. + (Currently it half assumes that it's passed a .mp4 filename.) +* See if other compression parameters would give better quality/speed/etc. +* Make the frame size variable. +* Tidy everything up a bit. + +Please complain if I forget to do those things. + +*/ + +#include "FFmpeg.h" + +#ifdef _MSC_VER +# pragma warning(disable: 4100 4505 4510 4610) +#endif + +#include "ffmpeg/avformat.h" +#include "ffmpeg/swscale.h" + +struct VideoEncoderImpl +{ + int framerate; + int bitrate; + float duration; + + AVStream *video_st; + AVFormatContext *oc; + AVOutputFormat *fmt; + + AVFrame *picture, *tmp_picture; + uint8_t *video_outbuf; + int frame_count, video_outbuf_size; + + VideoEncoderImpl() + { + video_st = NULL; + oc = NULL; + fmt = NULL; + + picture = NULL; + tmp_picture = NULL; + video_outbuf = NULL; + frame_count = 0; + video_outbuf_size = 0; + } + + AVStream *add_video_stream(AVFormatContext *oc, int codec_id) + { + AVCodecContext *c; + AVStream *st; + + st = av_new_stream(oc, 0); + if (!st) { + fprintf(stderr, "Could not alloc stream\n"); + exit(1); + } + + c = st->codec; + c->codec_id = (CodecID)codec_id; + c->codec_type = CODEC_TYPE_VIDEO; + + c->bit_rate = bitrate*1000; + c->width = 640; + c->height = 480; + + c->time_base.den = framerate; + c->time_base.num = 1; + + c->pix_fmt = PIX_FMT_YUV420P; + + // TODO: these compression parameters aren't necessarily any good + + c->mb_decision = FF_MB_DECISION_RD; + c->gop_size = 250; + c->max_b_frames = 2; + c->b_frame_strategy = 1; + c->dia_size = 4; + + c->me_cmp = c->me_sub_cmp= FF_CMP_SAD; + c->mb_cmp= FF_CMP_SSE; + + c->last_predictor_count = 3; + + c->flags |= CODEC_FLAG_4MV | CODEC_FLAG_QP_RD; + + + if(!strcmp(oc->oformat->name, "mp4") || !strcmp(oc->oformat->name, "mov") || !strcmp(oc->oformat->name, "3gp")) + c->flags |= CODEC_FLAG_GLOBAL_HEADER; + + return st; + } + + AVFrame *alloc_picture(int pix_fmt, int width, int height) + { + AVFrame *picture; + uint8_t *picture_buf; + int size; + + picture = avcodec_alloc_frame(); + if (!picture) + return NULL; + size = avpicture_get_size(pix_fmt, width, height); + picture_buf = (uint8_t*)av_malloc(size); + if (!picture_buf) { + av_free(picture); + return NULL; + } + avpicture_fill((AVPicture *)picture, picture_buf, + pix_fmt, width, height); + return picture; + } + + void open_video(AVFormatContext *oc, AVStream *st) + { + AVCodec *codec; + AVCodecContext *c; + + c = st->codec; + + /* find the video encoder */ + codec = avcodec_find_encoder(c->codec_id); + if (!codec) { + fprintf(stderr, "codec not found\n"); + exit(1); + } + + /* open the codec */ + if (avcodec_open(c, codec) < 0) { + fprintf(stderr, "could not open codec\n"); + exit(1); + } + + video_outbuf = NULL; + if (!(oc->oformat->flags & AVFMT_RAWPICTURE)) { + /* allocate output buffer */ + /* XXX: API change will be done */ + /* buffers passed into lav* can be allocated any way you prefer, + as long as they're aligned enough for the architecture, and + they're freed appropriately (such as using av_free for buffers + allocated with av_malloc) */ + video_outbuf_size = bitrate*1000/framerate * 8; + video_outbuf = (uint8_t*)av_malloc(video_outbuf_size); + } + + /* allocate the encoded raw picture */ + picture = alloc_picture(c->pix_fmt, c->width, c->height); + if (!picture) { + fprintf(stderr, "Could not allocate picture\n"); + exit(1); + } + + /* if the output format is not YUV420P, then a temporary YUV420P + picture is needed too. It is then converted to the required + output format */ + tmp_picture = NULL; + if (c->pix_fmt != PIX_FMT_RGB24) { + tmp_picture = alloc_picture(PIX_FMT_RGB24, c->width, c->height); + if (!tmp_picture) { + fprintf(stderr, "Could not allocate temporary picture\n"); + exit(1); + } + } + } + + void copy_rgb_image(AVFrame *pict, int width, int height, const unsigned char* buffer) + { + for(int y=0;ydata[0][y * pict->linesize[0]], buffer+y*width*3, width*3); + } + } + + void write_video_frame(AVFormatContext *oc, AVStream *st, const unsigned char* buffer) + { + AVCodecContext *c; + static struct SwsContext *img_convert_ctx; + int out_size, ret; + + c = st->codec; + + if (!buffer || frame_count >= (int)(duration*framerate)) { + /* no more frame to compress. The codec has a latency of a few + frames if using B frames, so we get the last frames by + passing the same picture again */ + } else { + if (c->pix_fmt != PIX_FMT_RGB24) { + /* as we only generate a YUV420P picture, we must convert it + to the codec pixel format if needed */ + if (img_convert_ctx == NULL) { + img_convert_ctx = sws_getContext(c->width, c->height, + PIX_FMT_RGB24, + c->width, c->height, + c->pix_fmt, + SWS_BICUBIC, NULL, NULL, NULL); + if (img_convert_ctx == NULL) { + fprintf(stderr, "Cannot initialize the conversion context\n"); + exit(1); + } + } + copy_rgb_image(tmp_picture, c->width, c->height, buffer); + sws_scale(img_convert_ctx, tmp_picture->data, tmp_picture->linesize, + 0, c->height, picture->data, picture->linesize); + } else { + copy_rgb_image(picture, c->width, c->height, buffer); + } + } + + if (oc->oformat->flags & AVFMT_RAWPICTURE) { + /* raw video case. The API will change slightly in the near + futur for that */ + AVPacket pkt; + av_init_packet(&pkt); + + pkt.flags |= PKT_FLAG_KEY; + pkt.stream_index= st->index; + pkt.data= (uint8_t *)picture; + pkt.size= sizeof(AVPicture); + + ret = av_write_frame(oc, &pkt); + } else { + /* encode the image */ + out_size = avcodec_encode_video(c, video_outbuf, video_outbuf_size, picture); + /* if zero size, it means the image was buffered */ + if (out_size > 0) { + AVPacket pkt; + av_init_packet(&pkt); + + pkt.pts= av_rescale_q(c->coded_frame->pts, c->time_base, st->time_base); + if(c->coded_frame->key_frame) + pkt.flags |= PKT_FLAG_KEY; + pkt.stream_index= st->index; + pkt.data= video_outbuf; + pkt.size= out_size; + + /* write the compressed frame in the media file */ + ret = av_write_frame(oc, &pkt); + } else { + ret = 0; + } + } + if (ret != 0) { + fprintf(stderr, "Error while writing video frame\n"); + exit(1); + } + frame_count++; + } + + void close_video(AVFormatContext *oc, AVStream *st) + { + avcodec_close(st->codec); + av_free(picture->data[0]); + av_free(picture); + if (tmp_picture) { + av_free(tmp_picture->data[0]); + av_free(tmp_picture); + } + av_free(video_outbuf); + } +}; + +////////////////////////////////////////////////////////////////////////// + +void log(void* v, int i, const char* format, va_list ap) +{ + char buf[512]; + vsnprintf(buf, sizeof(buf), format, ap); + buf[sizeof(buf)-1] = '\0'; + wxLogDebug(L"[%d] %hs", i, buf); +} + +VideoEncoder::VideoEncoder(const wxString& filenameStr, int framerate, int bitrate, float duration) +: m(new VideoEncoderImpl) +{ + wxCharBuffer filename = filenameStr.ToAscii(); + m->framerate = framerate; + m->bitrate = bitrate; + m->duration = duration; + + /* initialize libavcodec, and register all codecs and formats */ + av_register_all(); + + av_log_set_callback(&log); + + /* auto detect the output format from the name. default is mpeg. */ + m->fmt = guess_format(NULL, filename, NULL); + if (!m->fmt) { + printf("Could not deduce output format from file extension: using MPEG.\n"); + m->fmt = guess_format("mpeg", NULL, NULL); + } + if (!m->fmt) { + fprintf(stderr, "Could not find suitable output format\n"); + exit(1); + } + + /* allocate the output media context */ + m->oc = av_alloc_format_context(); + if (!m->oc) { + fprintf(stderr, "Memory error\n"); + exit(1); + } + m->oc->oformat = m->fmt; + strncpy(m->oc->filename, filename, sizeof(m->oc->filename)); + m->oc->filename[sizeof(m->oc->filename) - 1] = '\0'; + + /* add the audio and video streams using the default format codecs + and initialize the codecs */ + m->video_st = NULL; + if (m->fmt->video_codec != CODEC_ID_NONE) { + m->video_st = m->add_video_stream(m->oc, m->fmt->video_codec); + } + + /* set the output parameters (must be done even if no + parameters). */ + if (av_set_parameters(m->oc, NULL) < 0) { + fprintf(stderr, "Invalid output format parameters\n"); + exit(1); + } + + dump_format(m->oc, 0, filename, 1); + + /* now that all the parameters are set, we can open the audio and + video codecs and allocate the necessary encode buffers */ + if (m->video_st) + m->open_video(m->oc, m->video_st); + + /* open the output file, if needed */ + if (!(m->fmt->flags & AVFMT_NOFILE)) { + if (url_fopen(&m->oc->pb, filename, URL_WRONLY) < 0) { + fprintf(stderr, "Could not open '%s'\n", filename); + exit(1); + } + } + + /* write the stream header, if any */ + av_write_header(m->oc); + +} + +void VideoEncoder::Frame(const unsigned char* buffer) +{ + double video_pts = (double)m->video_st->pts.val * m->video_st->time_base.num / m->video_st->time_base.den; + if (video_pts >= m->duration) + return; + + m->write_video_frame(m->oc, m->video_st, buffer); +} + +VideoEncoder::~VideoEncoder() +{ + for(;;) + { + double video_pts = (double)m->video_st->pts.val * m->video_st->time_base.num / m->video_st->time_base.den; + if (video_pts >= m->duration) + break; + + m->write_video_frame(m->oc, m->video_st, NULL); + } + + + /* close each codec */ + if (m->video_st) + m->close_video(m->oc, m->video_st); + + /* write the trailer, if any */ + av_write_trailer(m->oc); + + /* free the streams */ + for(int i = 0; i < m->oc->nb_streams; i++) { + av_freep(&m->oc->streams[i]->codec); + av_freep(&m->oc->streams[i]); + } + + if (!(m->fmt->flags & AVFMT_NOFILE)) { + /* close the output file */ + url_fclose(&m->oc->pb); + } + + /* free the stream */ + av_free(m->oc); + + + delete m; +} diff --git a/source/tools/atlas/AtlasUI/General/VideoRecorder/FFmpeg.h b/source/tools/atlas/AtlasUI/General/VideoRecorder/FFmpeg.h new file mode 100644 index 0000000000..7217b36f38 --- /dev/null +++ b/source/tools/atlas/AtlasUI/General/VideoRecorder/FFmpeg.h @@ -0,0 +1,17 @@ +#ifndef FFMPEG_H__ +#define FFMPEG_H__ + +struct VideoEncoderImpl; + +class VideoEncoder +{ +public: + VideoEncoder(const wxString& filename, int framerate, int bitrate, float duration); + void Frame(const unsigned char* buffer); + ~VideoEncoder(); + +private: + VideoEncoderImpl* m; +}; + +#endif // FFMPEG_H__ diff --git a/source/tools/atlas/AtlasUI/General/VideoRecorder/VideoRecorder.cpp b/source/tools/atlas/AtlasUI/General/VideoRecorder/VideoRecorder.cpp new file mode 100644 index 0000000000..7db5d07b6e --- /dev/null +++ b/source/tools/atlas/AtlasUI/General/VideoRecorder/VideoRecorder.cpp @@ -0,0 +1,187 @@ +#include "stdafx.h" + +#include "VideoRecorder.h" + +#include "FFmpeg.h" + +#include "GameInterface/Messages.h" + +using namespace AtlasMessage; + +class RecorderDialog : public wxDialog +{ + enum + { + ID_FILENAME, + ID_FILECHOOSE, + ID_FRAMERATE, + ID_BITRATE, + ID_DURATION, + ID_FILESIZE + }; + +public: + RecorderDialog(wxWindow* parent, float duration) + : wxDialog(parent, wxID_ANY, (wxString)_("Video recording options")), + m_Duration(duration) + { + wxArrayString framerates; + framerates.Add(_T("10")); + framerates.Add(_T("15")); + framerates.Add(_T("25")); + framerates.Add(_T("30")); + framerates.Add(_T("60")); + wxString framerateDefault = _T("25"); + + wxArrayString bitrates; + bitrates.Add(_T("500")); + bitrates.Add(_T("800")); + bitrates.Add(_T("1200")); + bitrates.Add(_T("1600")); + bitrates.Add(_T("3200")); + wxString bitrateDefault = _T("1200"); + + + wxSizer* filenameSizer = new wxBoxSizer(wxHORIZONTAL); + filenameSizer->Add(new wxTextCtrl(this, ID_FILENAME, _T("")), wxSizerFlags().Proportion(1)); + filenameSizer->Add(new wxButton(this, ID_FILECHOOSE, _("Browse..."))); + + wxGridSizer* gridSizer = new wxGridSizer(4); + + gridSizer->Add(new wxStaticText(this, -1, _("Framerate (fps)")), wxSizerFlags().Right().Border(wxRIGHT, 4)); + gridSizer->Add(new wxComboBox(this, ID_FRAMERATE, framerateDefault, wxDefaultPosition, wxDefaultSize, framerates), wxSizerFlags().Proportion(1).Expand()); + // TODO: use a wxValidator to only allow digits + + gridSizer->Add(new wxStaticText(this, -1, _("Duration (seconds):")), wxSizerFlags().Right().Border(wxLEFT, 4)); + gridSizer->Add(new wxStaticText(this, ID_DURATION, wxString::Format(_T("%.1f"), m_Duration)), wxSizerFlags().Expand().Border(wxLEFT, 4)); + + gridSizer->Add(new wxStaticText(this, -1, _("Bitrate (Kbit/s)")), wxSizerFlags().Right().Border(wxRIGHT, 4)); + gridSizer->Add(new wxComboBox(this, ID_BITRATE, bitrateDefault, wxDefaultPosition, wxDefaultSize, bitrates), wxSizerFlags().Proportion(1).Expand()); + + gridSizer->Add(new wxStaticText(this, -1, _("Estimated file size:")), wxSizerFlags().Right().Border(wxRIGHT, 4)); + gridSizer->Add(new wxStaticText(this, ID_FILESIZE, L"???"), wxSizerFlags().Expand().Border(wxLEFT, 4)); + + RecalculateSizes_(); + + wxSizer* gridSizerBox = new wxStaticBoxSizer(wxVERTICAL, this, _("Compression options")); + gridSizerBox->Add(gridSizer, wxSizerFlags().Border(wxALL, 10)); + + wxStdDialogButtonSizer* buttonSizer = new wxStdDialogButtonSizer(); + wxButton* okButton = new wxButton(this, wxID_OK, _("Save")); + okButton->SetDefault(); + buttonSizer->AddButton(okButton); + buttonSizer->AddButton(new wxButton(this, wxID_CANCEL, _("Cancel"))); + buttonSizer->Realize(); + + wxSizer* sizer = new wxBoxSizer(wxVERTICAL); + + sizer->Add(filenameSizer, wxSizerFlags().Border(wxALL, 10).Expand()); + sizer->Add(gridSizerBox, wxSizerFlags().Border(wxALL, 10)); + sizer->Add(buttonSizer, wxSizerFlags().Border(wxALL, 10).Centre()); + + SetSizer(sizer); + sizer->SetSizeHints(this); + } + + // Outputs: + wxString m_Filename; + unsigned long m_Framerate; + unsigned long m_Bitrate; + +protected: + + void OnOK(wxCommandEvent& event) + { + wxTextCtrl* filenameWin = wxDynamicCast(FindWindow(ID_FILENAME), wxTextCtrl); + wxComboBox* framerateWin = wxDynamicCast(FindWindow(ID_FRAMERATE), wxComboBox); + wxComboBox* bitrateWin = wxDynamicCast(FindWindow(ID_BITRATE), wxComboBox); + + wxCHECK(filenameWin && framerateWin && bitrateWin, ); + + m_Filename = filenameWin->GetValue(); + if (m_Filename.IsEmpty()) + { + wxLogError(_("No filename specified.")); + return; + } + + if (!framerateWin->GetValue().ToULong(&m_Framerate) || + !bitrateWin->GetValue().ToULong(&m_Bitrate)) + { + wxLogError(_("Invalid framerate/bitrate.")); + return; + } + + wxDialog::OnOK(event); + } + +private: + + void RecalculateSizes(wxCommandEvent& WXUNUSED(event)) { RecalculateSizes_(); } + void RecalculateSizes_() + { + wxComboBox* framerateWin = wxDynamicCast(FindWindow(ID_FRAMERATE), wxComboBox); + wxComboBox* bitrateWin = wxDynamicCast(FindWindow(ID_BITRATE), wxComboBox); + wxStaticText* filesizeWin = wxDynamicCast(FindWindow(ID_FILESIZE), wxStaticText); + wxCHECK(framerateWin && bitrateWin && filesizeWin, ); + unsigned long framerate = 0, bitrate = 0; + if (!framerateWin->GetValue().ToULong(&framerate) || !bitrateWin->GetValue().ToULong(&bitrate)) + return; + + int size = m_Duration * bitrate * 1000/8; + wxString sizeStr; + if (size < 1024*1024) sizeStr = wxString::Format(_T("~%.0f KiB"), (float)size/1024); + else if (size < 1024*1024*100) sizeStr = wxString::Format(_T("~%.1f MiB"), (float)size/(1024*1024)); + else if (size < 1024*1024*1024) sizeStr = wxString::Format(_T("~%.0f MiB"), (float)size/(1024*1024)); + else sizeStr = wxString::Format(_T("~%.2f GiB"), (float)size/(1024*1024*1024)); + filesizeWin->SetTitle(sizeStr); + } + + void FileBrowse(wxCommandEvent& WXUNUSED(event)) + { + wxTextCtrl* filenameWin = wxDynamicCast(FindWindow(ID_FILENAME), wxTextCtrl); + wxCHECK(filenameWin, ); + + wxFileDialog dlg (this, wxFileSelectorPromptStr, filenameWin->GetValue(), filenameWin->GetValue(), _("MP4 files (*.mp4)|*.mp4"), wxSAVE); + if (dlg.ShowModal() != wxID_OK) + return; + + filenameWin->SetValue(dlg.GetPath()); + } + + float m_Duration; + + DECLARE_EVENT_TABLE(); +}; + +BEGIN_EVENT_TABLE(RecorderDialog, wxDialog) + EVT_COMBOBOX(ID_FRAMERATE, RecorderDialog::RecalculateSizes) + EVT_COMBOBOX(ID_BITRATE, RecorderDialog::RecalculateSizes) + EVT_TEXT(ID_FRAMERATE, RecorderDialog::RecalculateSizes) + EVT_TEXT(ID_BITRATE, RecorderDialog::RecalculateSizes) + EVT_BUTTON(ID_FILECHOOSE, RecorderDialog::FileBrowse) + EVT_BUTTON(wxID_OK, RecorderDialog::OnOK) +END_EVENT_TABLE() + + +void callback(const sCinemaRecordCB* data, void* cbdata) +{ + VideoEncoder* enc = (VideoEncoder*)cbdata; + enc->Frame(data->buffer); +} + +void VideoRecorder::RecordCinematic(wxWindow* window, const wxString& trackName, float duration) +{ + RecorderDialog dlg(window, duration); + if (dlg.ShowModal() != wxID_OK) + return; + + wxStopWatch sw; + + VideoEncoder venc (dlg.m_Filename, dlg.m_Framerate, dlg.m_Bitrate, duration); + + qCinemaRecord qry(trackName.c_str(), dlg.m_Framerate, duration, Callback(&callback, (void*)&venc)); + qry.Post(); + + wxLogMessage(_("Finished recording (took %.1f seconds)\n"), sw.Time()/1000.f); +} diff --git a/source/tools/atlas/AtlasUI/General/VideoRecorder/VideoRecorder.h b/source/tools/atlas/AtlasUI/General/VideoRecorder/VideoRecorder.h new file mode 100644 index 0000000000..620618a2c7 --- /dev/null +++ b/source/tools/atlas/AtlasUI/General/VideoRecorder/VideoRecorder.h @@ -0,0 +1,10 @@ +#ifndef VIDEORECORDER_H__ +#define VIDEORECORDER_H__ + +class VideoRecorder +{ +public: + static void RecordCinematic(wxWindow* window, const wxString& trackName, float duration); +}; + +#endif // VIDEORECORDER_H__ diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Cinematic/Cinematic.cpp b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Cinematic/Cinematic.cpp index a805960239..aef0d47fe4 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Cinematic/Cinematic.cpp +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Cinematic/Cinematic.cpp @@ -9,6 +9,8 @@ #include "ScenarioEditor/Tools/Common/Tools.h" #include "HighResTimer/HighResTimer.h" +#include "General/VideoRecorder/VideoRecorder.h" + #include "wx/spinctrl.h" #include "wx/filename.h" #include "wx/wfstream.h" @@ -23,7 +25,7 @@ struct eCinemaButton { enum { previous/*, rewind, reverse*/, stop, - play, pause, /*forward,*/ next }; + play, pause, /*forward,*/ next, record }; }; float CinemaTextFloat(wxTextCtrl&, size_t, float, float, float); @@ -925,6 +927,17 @@ public: m_Parent->m_Playing = true; } + void OnRecord(wxCommandEvent& WXUNUSED(event)) + { + if ( m_Parent->m_SelectedTrack < 0 ) + return; + m_Parent->m_SliderBox->m_Timer.Stop(); + m_Parent->m_SliderBox->PrepareTimers(); + + VideoRecorder::RecordCinematic(this, + m_Parent->m_Tracks[m_Parent->m_SelectedTrack].name.c_str(), + m_Parent->m_Tracks[m_Parent->m_SelectedTrack].duration); + } void OnPause(wxCommandEvent& WXUNUSED(event)) { if ( m_Parent->m_SelectedTrack < 0 ) @@ -980,6 +993,7 @@ BEGIN_EVENT_TABLE(CinemaButtonBox, wxPanel) EVT_BUTTON(eCinemaButton::pause, CinemaButtonBox::OnPause) // EVT_BUTTON(eCinemaButton::forward, CinemaButtonBox::OnForward) EVT_BUTTON(eCinemaButton::next, CinemaButtonBox::OnNext) + EVT_BUTTON(eCinemaButton::record, CinemaButtonBox::OnRecord) END_EVENT_TABLE() ////////////////////////////////////////////////////////////////////////// @@ -1126,7 +1140,7 @@ wxImage CinematicSidebar::LoadIcon(const wxString& filename) wxImage img (1, 1, true); // Load the icon - wxFileName iconPath (_T("mods/official/art/textures/ui/session/icons/single/atlas/")); + wxFileName iconPath (_T("tools/atlas/buttons/")); iconPath.MakeAbsolute(Datafile::GetDataDirectory()); iconPath.SetFullName(filename); wxFileInputStream fstr (iconPath.GetFullPath()); @@ -1162,6 +1176,9 @@ void CinematicSidebar::LoadIcons() wxBitmapButton* next = new wxBitmapButton(m_IconSizer, eCinemaButton::next, LoadIcon( _T("next_s.bmp") )); m_IconSizer->Add(next); + wxBitmapButton* record = new wxBitmapButton(m_IconSizer, + eCinemaButton::record, LoadIcon( _T("record_s.bmp") )); + m_IconSizer->Add(record); } void CinematicSidebar::AddTrack(float x, float y, float z, std::wstring& name, int count) diff --git a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Cinematic/Cinematic.h b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Cinematic/Cinematic.h index ddabc97916..6b8c2b7449 100644 --- a/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Cinematic/Cinematic.h +++ b/source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Cinematic/Cinematic.h @@ -94,13 +94,6 @@ private: CinemaInfoBox* m_InfoBox; // ^same^ CinematicBottomBar* m_CinemaBottomBar; - struct eCinemaButton - { - enum { - previous/*, rewind, reverse*/, stop, - play, pause, /*forward,*/ next }; - }; - wxImage LoadIcon(const wxString& filename); void LoadIcons(); }; diff --git a/source/tools/atlas/GameInterface/Handlers/MiscHandlers.cpp b/source/tools/atlas/GameInterface/Handlers/MiscHandlers.cpp index c078056f12..6a74742a1e 100644 --- a/source/tools/atlas/GameInterface/Handlers/MiscHandlers.cpp +++ b/source/tools/atlas/GameInterface/Handlers/MiscHandlers.cpp @@ -6,6 +6,13 @@ #include "ps/GameSetup/Config.h" #include "ps/Util.h" +#include "ps/Game.h" +#include "renderer/Renderer.h" +#include "ps/GameSetup/GameSetup.h" +#include "../GameLoop.h" + +extern void (*Atlas_GLSwapBuffers)(void* context); + namespace AtlasMessage { MESSAGEHANDLER(MessageTrace) @@ -19,6 +26,57 @@ MESSAGEHANDLER(Screenshot) WriteBigScreenshot("bmp", msg->tiles); } +QUERYHANDLER(CinemaRecord) +{ + CCinemaManager* manager = g_Game->GetView()->GetCinema(); + manager->SetCurrentTrack(*msg->track, false, false, false); + + const int w = 640, h = 480; + { + g_Renderer.Resize(w, h); + SViewPort vp = { 0, 0, w, h }; + g_Game->GetView()->GetCamera()->SetViewPort(&vp); + g_Game->GetView()->GetCamera()->SetProjection(CGameView::defaultNear, CGameView::defaultFar, CGameView::defaultFOV); + } + + unsigned char* img = new unsigned char [w*h*3]; + + int num_frames = msg->framerate * msg->duration; + + for (int frame = 0; frame < num_frames; ++frame) + { + manager->MoveToPointAbsolute((float)frame/msg->framerate); + Render(); + Atlas_GLSwapBuffers((void*)g_GameLoop->glContext); + glReadPixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, img); + + // Swap the rows around, else the image will be upside down + char temp[w*3]; + for (int y = 0; y < h/2; ++y) + { + memcpy2(temp, &img[y*w*3], w*3); + memcpy2(&img[y*w*3], &img[(h-1-y)*w*3], w*3); + memcpy2(&img[(h-1-y)*w*3], temp, w*3); + } + + // Call the user-supplied function with this data, so they can + // store it as a video + sCinemaRecordCB cbdata = { img }; + msg->cb.Call(cbdata); + } + + delete[] img; + + // Restore viewport + { + g_Renderer.Resize(g_xres, g_yres); + SViewPort vp = { 0, 0, g_xres, g_yres }; + g_Game->GetView()->GetCamera()->SetViewPort(&vp); + g_Game->GetView()->GetCamera()->SetProjection(CGameView::defaultNear, CGameView::defaultFar, CGameView::defaultFOV); + } + +} + QUERYHANDLER(Ping) { UNUSED2(msg); diff --git a/source/tools/atlas/GameInterface/Messages.h b/source/tools/atlas/GameInterface/Messages.h index 49758ab98a..aba3cb06c7 100644 --- a/source/tools/atlas/GameInterface/Messages.h +++ b/source/tools/atlas/GameInterface/Messages.h @@ -80,6 +80,20 @@ MESSAGE(Screenshot, ((int, tiles)) // the final image will be (640*tiles)x(480*tiles) ); +struct sCinemaRecordCB +{ + unsigned char* buffer; +}; +SHAREABLE_STRUCT(sCinemaRecordCB); + +QUERY(CinemaRecord, + ((std::wstring, track)) + ((int, framerate)) + ((float, duration)) + ((Callback, cb)) + , + ); + ////////////////////////////////////////////////////////////////////////// MESSAGE(Brush, diff --git a/source/tools/atlas/GameInterface/Shareable.h b/source/tools/atlas/GameInterface/Shareable.h index fef5de7791..c6f1277f83 100644 --- a/source/tools/atlas/GameInterface/Shareable.h +++ b/source/tools/atlas/GameInterface/Shareable.h @@ -268,6 +268,29 @@ public: } }; + +// Shareable callbacks: +// (TODO - this is probably not really safely shareable, due to unspecified calling conventions) +template struct Callback +{ + Callback(void (*cb) (const T*, void*), void* cbdata) : cb(cb), cbdata(cbdata) {} + void (*cb) (const T*, void*); + void* cbdata; +}; + +template class Shareable > +{ + ASSERT_TYPE_IS_SHAREABLE(T); +public: + Shareable(Callback cb) : cb(cb) {} + Callback cb; + void Call(const T& data) const + { + cb.cb(&data, cb.cbdata); + } +}; + + } #ifdef SHAREABLE_USED_NOMMGR