Add playblast: viewport capture to H.264 MP4 via libavcodec

- TimelinePanel: Playblast button opens modal with output dir (native
  folder picker via IFileOpenDialog), custom resolution (HD/FHD/4K
  presets), frame range, and movie export toggle
- UsdSceneRenderer: CaptureFrame() reads pixels from resolved MSAA FBO;
  Render() stores last dimensions for off-screen re-render at capture res
- MovieEncoder: streams RGBA frames directly into MP4 using libavcodec/
  libswscale (H.264, tries libx264 → nvenc → qsv → amf → openh264)
- Application: CapturePlayblastFrame() re-renders at capture resolution
  each frame; opens/writes/closes MovieEncoder inline with the render loop
- FileDialog: BrowseFolder() using Vista-style IFileOpenDialog (COM)
- CMake: FindFFmpeg.cmake locates prebuilt BtbN GPL shared package in
  third_party/ffmpeg; links and copies DLLs for main and test targets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 09:36:30 +08:00
parent c796c76561
commit 0538dd3243
12 changed files with 764 additions and 3 deletions
+36
View File
@@ -1,6 +1,7 @@
#include "FileDialog.h"
#include "Logger.h"
#include <commdlg.h>
#include <shobjidl.h>
#include <vector>
namespace UsdLayerManager {
@@ -56,4 +57,39 @@ std::string FileDialog::SaveFile(const char* filter, const char* title, const ch
return "";
}
std::string FileDialog::BrowseFolder(const char* title, HWND owner) {
IFileOpenDialog* pfd = nullptr;
if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd))))
return "";
DWORD opts = 0;
pfd->GetOptions(&opts);
pfd->SetOptions(opts | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);
if (title) {
int n = MultiByteToWideChar(CP_UTF8, 0, title, -1, nullptr, 0);
std::vector<wchar_t> wt(n);
MultiByteToWideChar(CP_UTF8, 0, title, -1, wt.data(), n);
pfd->SetTitle(wt.data());
}
std::string result;
if (SUCCEEDED(pfd->Show(owner))) {
IShellItem* psi = nullptr;
if (SUCCEEDED(pfd->GetResult(&psi))) {
PWSTR path = nullptr;
if (SUCCEEDED(psi->GetDisplayName(SIGDN_FILESYSPATH, &path)) && path) {
int n = WideCharToMultiByte(CP_UTF8, 0, path, -1, nullptr, 0, nullptr, nullptr);
result.resize(static_cast<size_t>(n) - 1);
WideCharToMultiByte(CP_UTF8, 0, path, -1, result.data(), n, nullptr, nullptr);
CoTaskMemFree(path);
}
psi->Release();
}
}
pfd->Release();
return result;
}
} // namespace UsdLayerManager
+6
View File
@@ -21,6 +21,12 @@ public:
const char* defaultExt = "usd",
HWND owner = nullptr
);
// Folder picker (Vista-style IFileOpenDialog)
static std::string BrowseFolder(
const char* title = "Select Output Folder",
HWND owner = nullptr
);
private:
static const int MAX_PATH_LENGTH = 4096;
+236
View File
@@ -0,0 +1,236 @@
#include "MovieEncoder.h"
#include "Logger.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
#include <string>
namespace UsdLayerManager {
// ---------------------------------------------------------------------------
// Impl holds all libav state.
// ---------------------------------------------------------------------------
struct MovieEncoder::Impl {
AVFormatContext* fmtCtx = nullptr;
AVCodecContext* encCtx = nullptr;
AVStream* stream = nullptr;
SwsContext* sws = nullptr;
AVFrame* frame = nullptr;
AVPacket* pkt = nullptr;
int64_t pts = 0;
int width = 0;
int height = 0;
};
// ---------------------------------------------------------------------------
MovieEncoder::MovieEncoder() = default;
MovieEncoder::~MovieEncoder() { Close(); Reset(); }
// ---------------------------------------------------------------------------
static std::string AvError(int code) {
char buf[128] = {};
av_strerror(code, buf, sizeof(buf));
return buf;
}
// ---------------------------------------------------------------------------
bool MovieEncoder::Open(const char* outputPath, int width, int height, double fps) {
if (m_open) Close();
Reset();
m_impl = new Impl();
m_impl->width = width;
m_impl->height = height;
// ── Format context ────────────────────────────────────────────────────
int ret = avformat_alloc_output_context2(&m_impl->fmtCtx,
nullptr, nullptr, outputPath);
if (ret < 0 || !m_impl->fmtCtx) {
m_error = "avformat_alloc_output_context2: " + AvError(ret);
Reset(); return false;
}
// ── Find encoder (prefer libx264, fall back to hardware or generic) ───
const char* encoderOrder[] = {
"libx264", "h264_nvenc", "h264_qsv", "h264_amf", "libopenh264", nullptr
};
const AVCodec* codec = nullptr;
for (int i = 0; encoderOrder[i]; ++i) {
codec = avcodec_find_encoder_by_name(encoderOrder[i]);
if (codec) { LOG_INFO(std::string("MovieEncoder: using ") + codec->name); break; }
}
if (!codec) codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
m_error = "No H.264 encoder found (install ffmpeg GPL or NVIDIA/Intel drivers)";
Reset(); return false;
}
// ── Stream ────────────────────────────────────────────────────────────
m_impl->stream = avformat_new_stream(m_impl->fmtCtx, nullptr);
if (!m_impl->stream) {
m_error = "avformat_new_stream failed";
Reset(); return false;
}
m_impl->stream->id = 0;
// ── Codec context ─────────────────────────────────────────────────────
m_impl->encCtx = avcodec_alloc_context3(codec);
if (!m_impl->encCtx) {
m_error = "avcodec_alloc_context3 failed";
Reset(); return false;
}
AVRational fpsRat = av_d2q(fps, 65536);
m_impl->encCtx->width = width;
m_impl->encCtx->height = height;
m_impl->encCtx->pix_fmt = AV_PIX_FMT_YUV420P;
m_impl->encCtx->framerate = fpsRat;
m_impl->encCtx->time_base = av_inv_q(fpsRat); // {1, fps_num}
// Reasonable quality defaults for CRF-capable encoders.
av_opt_set(m_impl->encCtx->priv_data, "crf", "23", AV_OPT_SEARCH_CHILDREN);
av_opt_set(m_impl->encCtx->priv_data, "preset", "fast", AV_OPT_SEARCH_CHILDREN);
// Ensure broad playback compatibility.
av_opt_set(m_impl->encCtx->priv_data, "profile", "high", AV_OPT_SEARCH_CHILDREN);
av_opt_set(m_impl->encCtx->priv_data, "level", "4.1", AV_OPT_SEARCH_CHILDREN);
if (m_impl->fmtCtx->oformat->flags & AVFMT_GLOBALHEADER)
m_impl->encCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
ret = avcodec_open2(m_impl->encCtx, codec, nullptr);
if (ret < 0) {
m_error = "avcodec_open2: " + AvError(ret);
Reset(); return false;
}
ret = avcodec_parameters_from_context(m_impl->stream->codecpar, m_impl->encCtx);
if (ret < 0) {
m_error = "avcodec_parameters_from_context: " + AvError(ret);
Reset(); return false;
}
m_impl->stream->time_base = m_impl->encCtx->time_base;
// ── Open output file ──────────────────────────────────────────────────
if (!(m_impl->fmtCtx->oformat->flags & AVFMT_NOFILE)) {
ret = avio_open(&m_impl->fmtCtx->pb, outputPath, AVIO_FLAG_WRITE);
if (ret < 0) {
m_error = std::string("avio_open(") + outputPath + "): " + AvError(ret);
Reset(); return false;
}
}
ret = avformat_write_header(m_impl->fmtCtx, nullptr);
if (ret < 0) {
m_error = "avformat_write_header: " + AvError(ret);
Reset(); return false;
}
// ── Frame + packet buffers ────────────────────────────────────────────
m_impl->frame = av_frame_alloc();
if (!m_impl->frame) {
m_error = "av_frame_alloc failed";
Reset(); return false;
}
m_impl->frame->width = width;
m_impl->frame->height = height;
m_impl->frame->format = AV_PIX_FMT_YUV420P;
ret = av_frame_get_buffer(m_impl->frame, 0);
if (ret < 0) {
m_error = "av_frame_get_buffer: " + AvError(ret);
Reset(); return false;
}
m_impl->pkt = av_packet_alloc();
if (!m_impl->pkt) {
m_error = "av_packet_alloc failed";
Reset(); return false;
}
// ── SwsContext: RGBA → YUV420P ────────────────────────────────────────
m_impl->sws = sws_getContext(
width, height, AV_PIX_FMT_RGBA,
width, height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!m_impl->sws) {
m_error = "sws_getContext failed";
Reset(); return false;
}
m_impl->pts = 0;
m_open = true;
return true;
}
// ---------------------------------------------------------------------------
bool MovieEncoder::WriteFrame(const uint8_t* rgba) {
if (!m_open || !m_impl) { m_error = "encoder not open"; return false; }
int ret = av_frame_make_writable(m_impl->frame);
if (ret < 0) { m_error = "av_frame_make_writable: " + AvError(ret); return false; }
const uint8_t* srcSlice[1] = { rgba };
int srcStride[1] = { m_impl->width * 4 };
sws_scale(m_impl->sws,
srcSlice, srcStride, 0, m_impl->height,
m_impl->frame->data, m_impl->frame->linesize);
m_impl->frame->pts = m_impl->pts++;
ret = avcodec_send_frame(m_impl->encCtx, m_impl->frame);
if (ret < 0) { m_error = "avcodec_send_frame: " + AvError(ret); return false; }
return DrainPackets();
}
// ---------------------------------------------------------------------------
bool MovieEncoder::Close() {
if (!m_open || !m_impl) return true;
// Flush
avcodec_send_frame(m_impl->encCtx, nullptr);
DrainPackets();
av_write_trailer(m_impl->fmtCtx);
if (m_impl->fmtCtx && !(m_impl->fmtCtx->oformat->flags & AVFMT_NOFILE))
avio_closep(&m_impl->fmtCtx->pb);
Reset();
m_open = false;
return true;
}
// ---------------------------------------------------------------------------
bool MovieEncoder::DrainPackets() {
int ret = 0;
while ((ret = avcodec_receive_packet(m_impl->encCtx, m_impl->pkt)) == 0) {
av_packet_rescale_ts(m_impl->pkt,
m_impl->encCtx->time_base,
m_impl->stream->time_base);
m_impl->pkt->stream_index = m_impl->stream->index;
ret = av_interleaved_write_frame(m_impl->fmtCtx, m_impl->pkt);
av_packet_unref(m_impl->pkt);
if (ret < 0) { m_error = "av_interleaved_write_frame: " + AvError(ret); return false; }
}
return ret == AVERROR(EAGAIN) || ret == AVERROR_EOF;
}
// ---------------------------------------------------------------------------
void MovieEncoder::Reset() {
if (!m_impl) return;
if (m_impl->sws) { sws_freeContext(m_impl->sws); m_impl->sws = nullptr; }
if (m_impl->pkt) { av_packet_free(&m_impl->pkt); m_impl->pkt = nullptr; }
if (m_impl->frame) { av_frame_free(&m_impl->frame); m_impl->frame = nullptr; }
if (m_impl->encCtx) { avcodec_free_context(&m_impl->encCtx); m_impl->encCtx = nullptr; }
if (m_impl->fmtCtx) { avformat_free_context(m_impl->fmtCtx); m_impl->fmtCtx = nullptr; }
delete m_impl;
m_impl = nullptr;
m_open = false;
}
} // namespace UsdLayerManager
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <string>
#include <cstdint>
namespace UsdLayerManager {
/// Streams RGBA frames directly into an H.264/MP4 file using libavcodec.
/// Usage:
/// MovieEncoder enc;
/// enc.Open("out.mp4", 1920, 1080, 24.0);
/// for each frame: enc.WriteFrame(rgbaPixels); // top-to-bottom, 4 bytes/px
/// enc.Close();
class MovieEncoder {
public:
MovieEncoder();
~MovieEncoder();
// Non-copyable
MovieEncoder(const MovieEncoder&) = delete;
MovieEncoder& operator=(const MovieEncoder&) = delete;
bool Open(const char* outputPath, int width, int height, double fps);
bool WriteFrame(const uint8_t* rgba);
bool Close(); // flush + write trailer; resets state for reuse
bool IsOpen() const { return m_open; }
const std::string& GetLastError() const { return m_error; }
private:
bool DrainPackets();
void Reset();
struct Impl;
Impl* m_impl = nullptr;
bool m_open = false;
std::string m_error;
};
} // namespace UsdLayerManager