Modify src structure

This commit is contained in:
2026-03-19 20:46:29 +08:00
parent 5d393a1e30
commit ae2c388716
14 changed files with 156 additions and 32 deletions
@@ -0,0 +1,112 @@
// AETemplateMediaPlane.mel
// Attribute Editor Template for MayaMediaPlaneNode
// This file customizes the appearance of MediaPlane node attributes in Maya's Attribute Editor
global proc AETemplateMediaPlane(string $nodeName)
{
editorTemplate -beginScrollLayout;
// Video File Section
editorTemplate -beginLayout "Video File" -collapse 0;
// Use custom control for video file with browse button
editorTemplate -callCustom "AETemplateMediaPlaneVideoFileCreate" "AETemplateMediaPlaneVideoFileUpdate" "videoFile";
editorTemplate -endLayout;
// Playback Section
editorTemplate -beginLayout "Playback" -collapse 0;
editorTemplate -addControl "currentTime";
editorTemplate -addControl "frameRate";
editorTemplate -addControl "playbackRate";
editorTemplate -addControl "useMayaFrameRate";
editorTemplate -addControl "loop";
editorTemplate -endLayout;
// Post Effects Section
editorTemplate -beginLayout "Post Effects" -collapse 0;
editorTemplate -addControl "postEffectCrop";
editorTemplate -addControl "postEffectResize";
editorTemplate -addControl "postEffectFlip";
editorTemplate -endLayout;
// Cache Section
editorTemplate -beginLayout "Cache" -collapse 0;
editorTemplate -addControl "cachePolicy";
editorTemplate -addControl "cacheSize";
editorTemplate -addControl "clearCache";
editorTemplate -addControl "outCacheHitRatio";
editorTemplate -endLayout;
// Output Section
editorTemplate -beginLayout "Output" -collapse 0;
editorTemplate -addControl "outFrameWidth";
editorTemplate -addControl "outFrameHeight";
editorTemplate -addControl "outFrameTimestamp";
editorTemplate -addControl "outFrameCount";
editorTemplate -addControl "outIsValid";
editorTemplate -endLayout;
// Add AE call to the base class
editorTemplate -addExtraControls;
editorTemplate -endScrollLayout;
}
// Custom control creation for video file with browse button
global proc AETemplateMediaPlaneVideoFileCreate(string $nodeName)
{
setUITemplate -pushTemplate attributeEditorTemplate;
// Create label and textField in a row
rowLayout -numberOfColumns 3
-columnWidth3 120 320 80
-adjustableColumn 2
-columnAlign3 "right" "center" "center"
-rowAttach 1 "left" 0;
text -label "videoFile";
textField -tx "" -width 320 "AETemplateMediaPlaneVideoFileTextField";
button -label "Browse..." -width 80
-command "AETemplateMediaPlaneBrowseButton()"
"AETemplateMediaPlaneBrowseButton";
setUITemplate -popTemplate;
}
// Update callback for video file control
global proc AETemplateMediaPlaneVideoFileUpdate(string $nodeName)
{
string $value = `getAttr ($nodeName + ".videoFile")`;
textField -edit -tx $value "AETemplateMediaPlaneVideoFileTextField";
}
// Browse button command
global proc AETemplateMediaPlaneBrowseButton()
{
// Get the current text field value
string $currentFile = `textField -q -tx "AETemplateMediaPlaneVideoFileTextField"`;
// Set up file filters
string $filters = "Video Files (*.mp4 *.mov *.avi *.mkv *.webm);;MP4 (*.mp4);;MOV (*.mov);;AVI (*.avi);;MKV (*.mkv);;WebM (*.webm);;All Files (*.*)";
// Open file dialog
string $result[] = `fileDialog2
-fileFilter $filters
-dialogStyle 2
-caption "Select Video File"
-startingDirectory ($currentFile != "" ? `dirname $currentFile` : "")`;
// If user selected a file, update the text field
if (size($result) > 0)
{
textField -edit -tx $result[0] "AETemplateMediaPlaneVideoFileTextField";
}
}
// Registration procedure
global proc AEregisterMediaPlaneNode()
{
// Register the AE template for MediaPlane node
// This is called from the plugin initialization
}
+126
View File
@@ -0,0 +1,126 @@
# MayaMediaPlaneNode Plugin CMakeLists.txt
# This is a standalone project for the Maya Media Plane Node plugin
cmake_minimum_required(VERSION 3.14)
# Set C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ============================================
# Find Maya and FFmpeg using CMake modules
# ============================================
# Add cmake modules path
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
# Find Maya
find_package(Maya REQUIRED)
# Find FFmpeg
find_package(FFmpeg REQUIRED)
# Include directories
include_directories(${MAYA_INCLUDE_DIR})
include_directories(${FFMPEG_INCLUDE_DIR})
# ============================================
# Source files
# ============================================
set(PLUGIN_SRCS
Plugin.cpp
MayaMediaPlaneNode.cpp
FFmpegVideoDecoder.cpp
FrameCache.cpp
)
set(MOD_FILES
MediaPlane.mod
)
set(MEL_SCRIPTS
AETemplateMediaPlane.mel
)
# ============================================
# Build Plugin
# ============================================
add_library(MayaMediaPlaneNode MODULE ${PLUGIN_SRCS})
set_target_properties(MayaMediaPlaneNode PROPERTIES
PREFIX "" # No prefix for Maya plugin
SUFFIX ".mll" # Maya plugin extension on Windows
OUTPUT_NAME "MayaMediaPlaneNode"
)
# Link libraries
target_link_libraries(MayaMediaPlaneNode PRIVATE
${MAYA_LIBRARIES}
${FFMPEG_LIBRARIES}
)
# Set Windows-specific properties
if(WIN32)
set_target_properties(MayaMediaPlaneNode PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS TRUE
COMPILE_FLAGS "/MDd"
)
endif()
# ============================================
# Install Rules
# ============================================
# Get Maya version from location
if(MAYA_LOCATION)
string(REGEX MATCH "Maya[0-9]{4}" MAYA_VERSION_NUM "${MAYA_LOCATION}")
if(NOT MAYA_VERSION_NUM)
set(MAYA_VERSION_NUM "Maya2023")
endif()
else()
set(MAYA_VERSION_NUM "Maya2023")
endif()
# Set install directories
set(PLUGIN_INSTALL_DIR "plug-ins/${MAYA_VERSION_NUM}")
set(BIN_INSTALL_DIR "bin")
# Install plugin
install(TARGETS MayaMediaPlaneNode
RUNTIME DESTINATION ${PLUGIN_INSTALL_DIR}
LIBRARY DESTINATION ${PLUGIN_INSTALL_DIR}
)
install(FILES ${MOD_FILES}
DESTINATION ${CMAKE_INSTALL_PREFIX}
)
# Install MEL scripts to Maya scripts directory
install(FILES ${MEL_SCRIPTS}
DESTINATION "scripts"
)
# Install FFmpeg DLLs
if(EXISTS "${FFMPEG_DLL_DIR}")
file(GLOB FFMPEG_DLLS "${FFMPEG_DLL_DIR}/*.dll")
foreach(DLL ${FFMPEG_DLLS})
if(EXISTS ${DLL})
get_filename_component(DLL_NAME ${DLL} NAME)
install(FILES ${DLL}
DESTINATION ${BIN_INSTALL_DIR}
)
endif()
endforeach()
endif()
# Print configuration summary
message(STATUS "===========================================")
message(STATUS "MayaMediaPlaneNode Configuration Summary")
message(STATUS "===========================================")
message(STATUS "Maya Version: ${MAYA_VERSION}")
message(STATUS "Maya Location: ${MAYA_LOCATION}")
message(STATUS "FFmpeg Root: ${FFMPEG_ROOT}")
message(STATUS "Install Prefix: ${CMAKE_INSTALL_PREFIX}")
message(STATUS "===========================================")
@@ -0,0 +1,479 @@
#include "FFmpegVideoDecoder.h"
#include <iostream>
#include <algorithm>
#include <cstring>
namespace MediaPlane {
// Static initialization
std::atomic<bool> FFmpegVideoDecoder::s_ffmpegInitialized{false};
FFmpegVideoDecoder::FFmpegVideoDecoder()
{
initializeFFmpeg();
}
FFmpegVideoDecoder::~FFmpegVideoDecoder()
{
close();
}
void FFmpegVideoDecoder::initializeFFmpeg()
{
if (!s_ffmpegInitialized.exchange(true)) {
// FFmpeg 4.0+ doesn't require av_register_all()
// Register all codecs and formats is done automatically
// av_register_all();
// Enable network for network streams
avformat_network_init();
}
}
bool FFmpegVideoDecoder::open(const std::string& filename)
{
std::lock_guard<std::mutex> lock(m_mutex);
// Close any existing file
close();
m_currentFile = filename;
// Open input file
int ret = avformat_open_input(&m_formatContext, filename.c_str(), nullptr, nullptr);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errorBuf, sizeof(errorBuf));
m_lastError = "Failed to open video file: " + std::string(errorBuf);
return false;
}
// Retrieve stream information
ret = avformat_find_stream_info(m_formatContext, nullptr);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errorBuf, sizeof(errorBuf));
m_lastError = "Failed to find stream info: " + std::string(errorBuf);
close();
return false;
}
// Find video stream
if (!findVideoStream()) {
close();
return false;
}
// Initialize codec context
if (!initCodecContext()) {
close();
return false;
}
// Allocate buffers
if (!allocateBuffers()) {
close();
return false;
}
// Update video info
m_videoInfo.width = m_codecContext->width;
m_videoInfo.height = m_codecContext->height;
m_videoInfo.pixelFormat = m_codecContext->pix_fmt;
// Get frame rate (may be in stream or codec context)
if (m_videoStream->avg_frame_rate.num > 0 && m_videoStream->avg_frame_rate.den > 0) {
m_videoInfo.frameRate = av_q2d(m_videoStream->avg_frame_rate);
} else if (m_codecContext->framerate.num > 0 && m_codecContext->framerate.den > 0) {
m_videoInfo.frameRate = av_q2d(m_codecContext->framerate);
} else {
m_videoInfo.frameRate = 24.0; // Default fallback
}
// Calculate total frame count
if (m_formatContext->duration != AV_NOPTS_VALUE) {
m_videoInfo.duration = (m_formatContext->duration + 500) / 1000; // Convert to milliseconds
m_videoInfo.frameCount = static_cast<int64_t>(m_videoInfo.frameRate *
(m_formatContext->duration / 1000000.0));
} else {
// Estimate from file size if duration is not available
m_videoInfo.frameCount = 0;
}
// Get codec name
if (m_codec) {
m_videoInfo.codecName = m_codec->name;
}
m_currentFrame = 0;
m_isAtEnd = false;
return true;
}
void FFmpegVideoDecoder::close()
{
std::lock_guard<std::mutex> lock(m_mutex);
freeBuffers();
if (m_codecContext) {
avcodec_free_context(&m_codecContext);
m_codecContext = nullptr;
}
if (m_formatContext) {
avformat_close_input(&m_formatContext);
m_formatContext = nullptr;
}
m_videoStream = nullptr;
m_codec = nullptr;
m_currentFile.clear();
m_lastError.clear();
m_currentFrame = 0;
m_isAtEnd = false;
}
bool FFmpegVideoDecoder::findVideoStream()
{
// Find the first video stream
for (unsigned int i = 0; i < m_formatContext->nb_streams; i++) {
if (m_formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
m_videoStream = m_formatContext->streams[i];
break;
}
}
if (!m_videoStream) {
m_lastError = "Could not find video stream in file";
return false;
}
return true;
}
bool FFmpegVideoDecoder::initCodecContext()
{
// Find decoder
m_codec = avcodec_find_decoder(m_videoStream->codecpar->codec_id);
if (!m_codec) {
m_lastError = "Could not find decoder for codec";
return false;
}
// Allocate codec context
m_codecContext = avcodec_alloc_context3(m_codec);
if (!m_codecContext) {
m_lastError = "Could not allocate codec context";
return false;
}
// Copy codec parameters to context
int ret = avcodec_parameters_to_context(m_codecContext, m_videoStream->codecpar);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errorBuf, sizeof(errorBuf));
m_lastError = "Could not copy codec parameters: " + std::string(errorBuf);
return false;
}
// Enable multi-threaded decoding
m_codecContext->thread_count = 0; // Use as many threads as available
// Open codec
ret = avcodec_open2(m_codecContext, m_codec, nullptr);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errorBuf, sizeof(errorBuf));
m_lastError = "Could not open codec: " + std::string(errorBuf);
return false;
}
return true;
}
bool FFmpegVideoDecoder::allocateBuffers()
{
// Allocate packet
m_packet = av_packet_alloc();
if (!m_packet) {
m_lastError = "Could not allocate packet";
return false;
}
// Allocate frames
m_tempFrame = av_frame_alloc();
m_rgbFrame = av_frame_alloc();
if (!m_tempFrame || !m_rgbFrame) {
m_lastError = "Could not allocate frames";
return false;
}
// Allocate RGB buffer
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24,
m_codecContext->width,
m_codecContext->height,
1);
m_rgbFrame->data[0] = (uint8_t*)av_malloc(numBytes * sizeof(uint8_t));
if (!m_rgbFrame->data[0]) {
m_lastError = "Could not allocate RGB buffer";
return false;
}
// Assign buffer to frame
av_image_fill_arrays(m_rgbFrame->data,
m_rgbFrame->linesize,
m_rgbFrame->data[0],
AV_PIX_FMT_RGB24,
m_codecContext->width,
m_codecContext->height,
1);
// Initialize SWS context for software scaling
m_swsContext = sws_getContext(m_codecContext->width,
m_codecContext->height,
m_codecContext->pix_fmt,
m_codecContext->width,
m_codecContext->height,
AV_PIX_FMT_RGB24,
SWS_BILINEAR,
nullptr,
nullptr,
nullptr);
if (!m_swsContext) {
m_lastError = "Could not initialize sws context";
return false;
}
return true;
}
void FFmpegVideoDecoder::freeBuffers()
{
if (m_swsContext) {
sws_freeContext(m_swsContext);
m_swsContext = nullptr;
}
if (m_rgbFrame) {
if (m_rgbFrame->data[0]) {
av_freep(&m_rgbFrame->data[0]);
}
av_frame_free(&m_rgbFrame);
}
av_frame_free(&m_tempFrame);
av_packet_free(&m_packet);
}
bool FFmpegVideoDecoder::decodeFrame(AVFrame* outputFrame)
{
int ret;
// Read frames until we get a video frame
while (true) {
ret = av_read_frame(m_formatContext, m_packet);
if (ret < 0) {
if (ret == AVERROR_EOF) {
m_isAtEnd = true;
}
return false;
}
// Check if packet belongs to video stream
if (m_packet->stream_index == m_videoStream->index) {
ret = avcodec_send_packet(m_codecContext, m_packet);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errorBuf, sizeof(errorBuf));
m_lastError = "Error sending packet to decoder: " + std::string(errorBuf);
av_packet_unref(m_packet);
return false;
}
ret = avcodec_receive_frame(m_codecContext, m_tempFrame);
if (ret < 0) {
if (ret == AVERROR(EAGAIN)) {
// Need more packets
av_packet_unref(m_packet);
continue;
} else if (ret == AVERROR_EOF) {
m_isAtEnd = true;
av_packet_unref(m_packet);
return false;
}
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errorBuf, sizeof(errorBuf));
m_lastError = "Error receiving frame from decoder: " + std::string(errorBuf);
av_packet_unref(m_packet);
return false;
}
// Successfully decoded frame
av_frame_copy(outputFrame, m_tempFrame);
av_frame_copy_props(outputFrame, m_tempFrame);
outputFrame->pts = m_tempFrame->pts;
outputFrame->best_effort_timestamp = m_tempFrame->best_effort_timestamp;
av_packet_unref(m_packet);
return true;
}
av_packet_unref(m_packet);
}
return false;
}
FFmpegVideoDecoder::FrameData FFmpegVideoDecoder::convertFrame(AVFrame* srcFrame)
{
FrameData result;
result.width = m_codecContext->width;
result.height = m_codecContext->height;
result.pts = srcFrame->pts;
result.timestamp = frameToTimestamp(m_currentFrame.load());
// Convert the frame to RGB
sws_scale(m_swsContext,
srcFrame->data,
srcFrame->linesize,
0,
m_codecContext->height,
m_rgbFrame->data,
m_rgbFrame->linesize);
// Allocate output buffer
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24,
m_codecContext->width,
m_codecContext->height,
1);
result.data = (uint8_t*)av_malloc(numBytes);
if (!result.data) {
m_lastError = "Could not allocate output buffer";
return result;
}
// Copy data
std::memcpy(result.data, m_rgbFrame->data[0], numBytes);
result.lineSize = m_rgbFrame->linesize[0];
return result;
}
FFmpegVideoDecoder::FrameData FFmpegVideoDecoder::readNextFrame()
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!isOpen() || m_isAtEnd) {
return FrameData();
}
if (decodeFrame(m_tempFrame)) {
m_currentFrame++;
return convertFrame(m_tempFrame);
}
return FrameData();
}
FFmpegVideoDecoder::FrameData FFmpegVideoDecoder::getFrame(int64_t frameIndex)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!isOpen()) {
return FrameData();
}
// If seeking backwards, we need to reopen the file
if (frameIndex < m_currentFrame.load()) {
avformat_seek_file(m_formatContext, m_videoStream->index,
0, 0, m_videoStream->index, AVSEEK_FLAG_BACKWARD);
avcodec_flush_buffers(m_codecContext);
m_currentFrame = 0;
m_isAtEnd = false;
}
// Seek to the target frame
if (!seekToFrame(frameIndex)) {
return FrameData();
}
// Read frames until we reach the target
while (m_currentFrame.load() <= frameIndex && !m_isAtEnd.load()) {
if (decodeFrame(m_tempFrame)) {
int64_t currentFrameNum = m_currentFrame.load();
m_currentFrame++;
if (currentFrameNum == frameIndex) {
return convertFrame(m_tempFrame);
}
} else {
break;
}
}
return FrameData();
}
bool FFmpegVideoDecoder::seekToFrame(int64_t frameIndex)
{
if (!m_videoStream) {
return false;
}
// Calculate timestamp
double timestamp = frameToTimestamp(frameIndex);
int64_t seekTarget = static_cast<int64_t>(timestamp * AV_TIME_BASE);
int ret = av_seek_frame(m_formatContext, m_videoStream->index,
seekTarget, AVSEEK_FLAG_BACKWARD);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errorBuf, sizeof(errorBuf));
m_lastError = "Seek failed: " + std::string(errorBuf);
return false;
}
// Flush codec buffers after seeking
avcodec_flush_buffers(m_codecContext);
m_currentFrame = frameIndex;
m_isAtEnd = false;
return true;
}
void FFmpegVideoDecoder::reset()
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_formatContext && m_videoStream) {
avformat_seek_file(m_formatContext, m_videoStream->index,
0, 0, m_videoStream->index, AVSEEK_FLAG_BACKWARD);
avcodec_flush_buffers(m_codecContext);
}
m_currentFrame = 0;
m_isAtEnd = false;
}
int64_t FFmpegVideoDecoder::timestampToFrame(double timestamp) const
{
if (m_videoInfo.frameRate <= 0) {
return 0;
}
return static_cast<int64_t>(timestamp * m_videoInfo.frameRate);
}
double FFmpegVideoDecoder::frameToTimestamp(int64_t frameIndex) const
{
if (m_videoInfo.frameRate <= 0) {
return 0.0;
}
return static_cast<double>(frameIndex) / m_videoInfo.frameRate;
}
} // namespace MediaPlane
+276
View File
@@ -0,0 +1,276 @@
#ifndef FFMPEG_VIDEO_DECODER_H
#define FFMPEG_VIDEO_DECODER_H
#ifdef _WIN32
// Disable warnings for FFmpeg
#pragma warning(push)
#pragma warning(disable: 4244)
#pragma warning(disable: 4267)
#endif
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
#ifdef _WIN32
#pragma warning(pop)
#endif
#include <string>
#include <memory>
#include <mutex>
#include <atomic>
namespace MediaPlane {
/**
* FFmpegVideoDecoder - Handles video decoding using FFmpeg libraries
*
* This class provides functionality to:
* - Open and decode MP4 video files
* - Extract video frames as RGB/RGBA pixel data
* - Provide frame-by-frame access for Maya timeline synchronization
*/
class FFmpegVideoDecoder {
public:
/**
* Video information structure containing metadata about the video
*/
struct VideoInfo {
int width = 0;
int height = 0;
double frameRate = 0.0;
int64_t frameCount = 0;
int64_t duration = 0; // in milliseconds
AVPixelFormat pixelFormat = AV_PIX_FMT_NONE;
std::string codecName;
bool isValid() const { return width > 0 && height > 0 && frameRate > 0; }
};
/**
* Decoded frame data
*/
struct FrameData {
uint8_t* data = nullptr; // Raw pixel data (RGB or RGBA)
int width = 0;
int height = 0;
int64_t pts = 0; // Presentation timestamp
double timestamp = 0.0; // Timestamp in seconds
int lineSize = 0; // Line size for pixel data
FrameData() = default;
~FrameData() {
if (data) {
av_freep(&data);
}
}
// Prevent copying
FrameData(const FrameData&) = delete;
FrameData& operator=(const FrameData&) = delete;
// Allow moving
FrameData(FrameData&& other) noexcept
: data(other.data)
, width(other.width)
, height(other.height)
, pts(other.pts)
, timestamp(other.timestamp)
, lineSize(other.lineSize)
{
other.data = nullptr;
other.width = 0;
other.height = 0;
}
FrameData& operator=(FrameData&& other) noexcept {
if (this != &other) {
if (data) {
av_freep(&data);
}
data = other.data;
width = other.width;
height = other.height;
pts = other.pts;
timestamp = other.timestamp;
lineSize = other.lineSize;
other.data = nullptr;
other.width = 0;
other.height = 0;
}
return *this;
}
};
public:
FFmpegVideoDecoder();
~FFmpegVideoDecoder();
// Non-copyable
FFmpegVideoDecoder(const FFmpegVideoDecoder&) = delete;
FFmpegVideoDecoder& operator=(const FFmpegVideoDecoder&) = delete;
/**
* Open a video file and initialize FFmpeg context
* @param filename Path to the video file
* @return true if successful, false otherwise
*/
bool open(const std::string& filename);
/**
* Close the video file and release FFmpeg resources
*/
void close();
/**
* Check if a video is currently open
* @return true if video is open
*/
bool isOpen() const { return m_formatContext != nullptr; }
/**
* Get video information
* @return VideoInfo structure containing video metadata
*/
const VideoInfo& getVideoInfo() const { return m_videoInfo; }
/**
* Decode and return a specific frame
* @param frameIndex Index of the frame to decode (0-based)
* @return FrameData containing the decoded frame, or empty on failure
*/
FrameData getFrame(int64_t frameIndex);
/**
* Decode the next frame in sequence
* @return FrameData containing the decoded frame, or empty on failure
*/
FrameData readNextFrame();
/**
* Seek to a specific frame
* @param frameIndex Index of the frame to seek to
* @return true if successful
*/
bool seekToFrame(int64_t frameIndex);
/**
* Get the current frame position
* @return Current frame index
*/
int64_t getCurrentFrame() const { return m_currentFrame.load(); }
/**
* Reset decoder to beginning
*/
void reset();
/**
* Get the last error message
* @return Error message string
*/
std::string getLastError() const { return m_lastError; }
/**
* Check if the video has ended
* @return true if at end of video
*/
bool isAtEnd() const { return m_isAtEnd.load(); }
private:
/**
* Initialize FFmpeg (call once at startup)
*/
static void initializeFFmpeg();
/**
* Find the video stream in the format context
* @return true if video stream found
*/
bool findVideoStream();
/**
* Initialize the codec context
* @return true if successful
*/
bool initCodecContext();
/**
* Allocate frame buffers
* @return true if successful
*/
bool allocateBuffers();
/**
* Free allocated buffers
*/
void freeBuffers();
/**
* Decode a frame from the video stream
* @param outputFrame Pointer to store the decoded frame
* @return true if frame was decoded
*/
bool decodeFrame(AVFrame* outputFrame);
/**
* Convert frame to RGB/RGBA format
* @param srcFrame Source frame to convert
* @return FrameData containing converted frame
*/
FrameData convertFrame(AVFrame* srcFrame);
/**
* Calculate frame index from timestamp
* @param timestamp Timestamp in seconds
* @return Frame index
*/
int64_t timestampToFrame(double timestamp) const;
/**
* Calculate timestamp from frame index
* @param frameIndex Frame index
* @return Timestamp in seconds
*/
double frameToTimestamp(int64_t frameIndex) const;
private:
// FFmpeg structures
AVFormatContext* m_formatContext = nullptr;
AVCodecContext* m_codecContext = nullptr;
AVStream* m_videoStream = nullptr;
const AVCodec* m_codec = nullptr;
// Frame buffers
AVFrame* m_rgbFrame = nullptr;
AVFrame* m_tempFrame = nullptr;
AVPacket* m_packet = nullptr;
SwsContext* m_swsContext = nullptr;
// Video information
VideoInfo m_videoInfo;
// State
std::atomic<int64_t> m_currentFrame{0};
std::atomic<bool> m_isAtEnd{false};
std::string m_lastError;
std::string m_currentFile;
// Thread safety
std::mutex m_mutex;
// FFmpeg initialization flag
static std::atomic<bool> s_ffmpegInitialized;
};
// Type alias for shared pointer
using FFmpegVideoDecoderPtr = std::shared_ptr<FFmpegVideoDecoder>;
} // namespace MediaPlane
#endif // FFMPEG_VIDEO_DECODER_H
+308
View File
@@ -0,0 +1,308 @@
#include "FrameCache.h"
#ifdef _WIN32
#pragma warning(push)
#pragma warning(disable: 4244)
#pragma warning(disable: 4267)
#endif
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
}
#ifdef _WIN32
#pragma warning(pop)
#endif
#include <algorithm>
namespace MediaPlane {
FrameCache::FrameCache(size_t maxMemory, size_t maxFrames, CachePolicy policy)
: m_maxMemory(maxMemory)
, m_maxFrameCount(maxFrames)
, m_policy(policy)
{
}
FrameCache::~FrameCache()
{
clear();
}
bool FrameCache::getFrame(int64_t frameIndex, AVFrame*& outputFrame)
{
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_cache.find(frameIndex);
if (it != m_cache.end()) {
// Frame found in cache
m_hits++;
// Update LRU
updateAccess(frameIndex);
// Reference the frame for the caller
if (it->second.frame) {
outputFrame = it->second.frame;
av_frame_ref(outputFrame, it->second.frame);
} else {
outputFrame = nullptr;
}
return true;
}
// Frame not found
m_misses++;
outputFrame = nullptr;
return false;
}
bool FrameCache::putFrame(int64_t frameIndex, AVFrame* frame)
{
if (!frame) {
return false;
}
std::lock_guard<std::mutex> lock(m_mutex);
// Calculate memory needed for this frame
size_t frameMemory = calculateFrameSize(frame);
// Check if we need to evict frames
if (m_currentMemory.load() + frameMemory > m_maxMemory ||
m_cache.size() >= m_maxFrameCount) {
evictFrames(frameMemory);
}
// Check if frame already exists (update instead)
auto it = m_cache.find(frameIndex);
if (it != m_cache.end()) {
// Update existing entry
m_currentMemory -= it->second.memorySize;
it->second.memorySize = frameMemory;
it->second.lastAccess = std::chrono::steady_clock::now();
// Update frame reference
if (it->second.frame) {
av_frame_free(&it->second.frame);
}
it->second.frame = frame;
av_frame_ref(it->second.frame, frame);
// Update LRU
m_lruList.remove(frameIndex);
m_lruList.push_back(frameIndex);
m_currentMemory += frameMemory;
return true;
}
// Create new entry
CacheEntry entry;
entry.frameIndex = frameIndex;
entry.frame = frame;
av_frame_ref(entry.frame, frame);
entry.memorySize = frameMemory;
entry.lastAccess = std::chrono::steady_clock::now();
// Insert into cache
m_cache[frameIndex] = std::move(entry);
m_lruList.push_back(frameIndex);
m_currentMemory += frameMemory;
return true;
}
bool FrameCache::contains(int64_t frameIndex) const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_cache.find(frameIndex) != m_cache.end();
}
void FrameCache::clear()
{
std::lock_guard<std::mutex> lock(m_mutex);
// Free all frames
for (auto& pair : m_cache) {
if (pair.second.frame) {
av_frame_free(&pair.second.frame);
}
}
m_cache.clear();
m_lruList.clear();
m_currentMemory = 0;
}
size_t FrameCache::size() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_cache.size();
}
size_t FrameCache::memoryUsage() const
{
return m_currentMemory.load();
}
void FrameCache::setMaxMemorySize(size_t maxBytes)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_maxMemory = maxBytes;
// Evict frames if we're over limit
if (m_currentMemory.load() > m_maxMemory) {
evictFrames(m_currentMemory.load() - m_maxMemory);
}
}
void FrameCache::setMaxFrameCount(size_t maxFrames)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_maxFrameCount = maxFrames;
// Evict frames if we're over limit
if (m_cache.size() >= m_maxFrameCount) {
size_t framesToEvict = m_cache.size() - m_maxFrameCount + 1;
for (size_t i = 0; i < framesToEvict && !m_lruList.empty(); ++i) {
int64_t oldestFrame = m_lruList.front();
removeFrame(oldestFrame);
}
}
}
void FrameCache::setPolicy(CachePolicy policy)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_policy = policy;
}
FrameCache::CacheStats FrameCache::getStats() const
{
std::lock_guard<std::mutex> lock(m_statsMutex);
CacheStats stats;
stats.cacheSize = m_cache.size();
stats.memoryUsage = m_currentMemory.load();
stats.maxMemory = m_maxMemory;
stats.maxFrameCount = m_maxFrameCount;
stats.hits = m_hits.load();
stats.misses = m_misses.load();
uint64_t total = stats.hits + stats.misses;
if (total > 0) {
stats.hitRatio = static_cast<double>(stats.hits) / static_cast<double>(total);
}
return stats;
}
void FrameCache::resetStats()
{
m_hits = 0;
m_misses = 0;
}
void FrameCache::invalidate()
{
clear();
resetStats();
}
const FrameCache::CacheEntry* FrameCache::getEntryAt(size_t position) const
{
std::lock_guard<std::mutex> lock(m_mutex);
if (position >= m_lruList.size()) {
return nullptr;
}
auto it = m_lruList.begin();
std::advance(it, position);
int64_t frameIndex = *it;
auto cacheIt = m_cache.find(frameIndex);
if (cacheIt != m_cache.end()) {
return &cacheIt->second;
}
return nullptr;
}
size_t FrameCache::calculateFrameSize(AVFrame* frame) const
{
if (!frame) {
return 0;
}
size_t size = 0;
// Calculate size for each plane
for (int i = 0; i < AV_NUM_DATA_POINTERS; i++) {
if (frame->data[i]) {
size += frame->linesize[i] * frame->height;
}
}
return size;
}
void FrameCache::evictFrames(size_t neededMemory)
{
// Evict from least recently used
while (!m_lruList.empty() &&
(m_currentMemory.load() > m_maxMemory ||
m_cache.size() >= m_maxFrameCount)) {
int64_t oldestFrame = m_lruList.front();
// Calculate how much memory this frame uses
size_t frameMemory = 0;
auto it = m_cache.find(oldestFrame);
if (it != m_cache.end()) {
frameMemory = it->second.memorySize;
}
removeFrame(oldestFrame);
}
}
void FrameCache::updateAccess(int64_t frameIndex)
{
// Move to end of LRU list (most recently used)
m_lruList.remove(frameIndex);
m_lruList.push_back(frameIndex);
// Update access time in cache entry
auto it = m_cache.find(frameIndex);
if (it != m_cache.end()) {
it->second.lastAccess = std::chrono::steady_clock::now();
it->second.accessCount++;
}
}
void FrameCache::removeFrame(int64_t frameIndex)
{
auto it = m_cache.find(frameIndex);
if (it != m_cache.end()) {
// Free frame memory
if (it->second.frame) {
av_frame_free(&it->second.frame);
}
// Update memory counter
m_currentMemory -= it->second.memorySize;
// Remove from cache
m_cache.erase(it);
// Remove from LRU list
m_lruList.remove(frameIndex);
}
}
} // namespace MediaPlane
+258
View File
@@ -0,0 +1,258 @@
#ifndef FRAME_CACHE_H
#define FRAME_CACHE_H
#include <cstdint>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <list>
#include <atomic>
#include <chrono>
// Forward declare AVFrame to avoid FFmpeg header dependency in header
struct AVFrame;
namespace MediaPlane {
/**
* FrameCache - LRU cache for video frames
*
* This class implements a thread-safe LRU (Least Recently Used) cache
* for decoded video frames to improve playback performance.
*/
class FrameCache {
public:
/**
* Cache policy types
*/
enum class CachePolicy {
CacheAll, // Cache all frames (memory permitting)
CacheRecent, // Cache only most recent frames (sliding window)
CacheKeyframes // Cache only keyframes for fast seeking
};
/**
* Cache statistics
*/
struct CacheStats {
size_t cacheSize = 0; // Number of frames cached
size_t memoryUsage = 0; // Memory usage in bytes
size_t maxMemory = 0; // Maximum memory limit
size_t maxFrameCount = 0; // Maximum frame count limit
uint64_t hits = 0; // Cache hits
uint64_t misses = 0; // Cache misses
double hitRatio = 0.0; // Hit ratio
void reset() {
hits = 0;
misses = 0;
hitRatio = 0.0;
}
};
/**
* Cache entry containing frame data and metadata
*/
struct CacheEntry {
AVFrame* frame = nullptr; // Decoded frame (reference counted)
int64_t frameIndex = 0; // Frame index in video
size_t memorySize = 0; // Memory used by this frame
std::chrono::steady_clock::time_point lastAccess; // For LRU
int accessCount = 0; // For LFU (future use)
bool dirty = false; // Modified by post-effects
CacheEntry() : lastAccess(std::chrono::steady_clock::now()) {}
~CacheEntry() {
// Frame will be freed by caller or separately
frame = nullptr;
}
// Non-copyable
CacheEntry(const CacheEntry&) = delete;
CacheEntry& operator=(const CacheEntry&) = delete;
// Movable
CacheEntry(CacheEntry&& other) noexcept
: frame(other.frame)
, frameIndex(other.frameIndex)
, memorySize(other.memorySize)
, lastAccess(other.lastAccess)
, accessCount(other.accessCount)
, dirty(other.dirty)
{
other.frame = nullptr;
other.frameIndex = 0;
other.memorySize = 0;
}
CacheEntry& operator=(CacheEntry&& other) noexcept {
if (this != &other) {
frame = other.frame;
frameIndex = other.frameIndex;
memorySize = other.memorySize;
lastAccess = other.lastAccess;
accessCount = other.accessCount;
dirty = other.dirty;
other.frame = nullptr;
other.frameIndex = 0;
other.memorySize = 0;
}
return *this;
}
};
public:
/**
* Constructor
* @param maxMemory Maximum memory usage in bytes (default 256MB)
* @param maxFrames Maximum number of frames to cache (default 100)
* @param policy Cache policy to use
*/
FrameCache(size_t maxMemory = 256 * 1024 * 1024,
size_t maxFrames = 100,
CachePolicy policy = CachePolicy::CacheRecent);
~FrameCache();
// Non-copyable
FrameCache(const FrameCache&) = delete;
FrameCache& operator=(const FrameCache&) = delete;
/**
* Get a frame from the cache
* @param frameIndex Index of the frame to retrieve
* @param outputFrame Pointer to store the retrieved frame (will be referenced)
* @return true if frame was found in cache
*/
bool getFrame(int64_t frameIndex, AVFrame*& outputFrame);
/**
* Put a frame into the cache
* @param frameIndex Index of the frame
* @param frame AVFrame to cache (will be referenced, not copied)
* @return true if frame was successfully cached
*/
bool putFrame(int64_t frameIndex, AVFrame* frame);
/**
* Check if a frame is in the cache
* @param frameIndex Index of the frame
* @return true if frame is cached
*/
bool contains(int64_t frameIndex) const;
/**
* Clear all frames from the cache
*/
void clear();
/**
* Get the current number of cached frames
* @return Number of frames in cache
*/
size_t size() const;
/**
* Get the current memory usage
* @return Memory usage in bytes
*/
size_t memoryUsage() const;
/**
* Set maximum memory size
* @param maxBytes Maximum memory in bytes
*/
void setMaxMemorySize(size_t maxBytes);
/**
* Set maximum frame count
* @param maxFrames Maximum number of frames
*/
void setMaxFrameCount(size_t maxFrames);
/**
* Set cache policy
* @param policy New cache policy
*/
void setPolicy(CachePolicy policy);
/**
* Get cache statistics
* @return CacheStats structure with current statistics
*/
CacheStats getStats() const;
/**
* Reset statistics (hits/misses)
*/
void resetStats();
/**
* Invalidate cache when video changes
*/
void invalidate();
/**
* Get the frame at a specific position (without affecting LRU)
* @param position Position in cache list (0 = oldest)
* @return Pointer to cache entry or nullptr if position invalid
*/
const CacheEntry* getEntryAt(size_t position) const;
private:
/**
* Calculate memory size of a frame
* @param frame Frame to calculate size for
* @return Memory size in bytes
*/
size_t calculateFrameSize(AVFrame* frame) const;
/**
* Evict frames according to LRU policy
* @param neededMemory Memory needed to free
*/
void evictFrames(size_t neededMemory);
/**
* Update access time for LRU
* @param frameIndex Index of accessed frame
*/
void updateAccess(int64_t frameIndex);
/**
* Remove a specific frame from cache
* @param frameIndex Index of frame to remove
*/
void removeFrame(int64_t frameIndex);
private:
// Cache storage - using unordered_map for O(1) lookup
std::unordered_map<int64_t, CacheEntry> m_cache;
// LRU list - tracks access order
std::list<int64_t> m_lruList;
// Limits
size_t m_maxMemory;
size_t m_maxFrameCount;
CachePolicy m_policy;
// Current memory usage
std::atomic<size_t> m_currentMemory{0};
// Statistics
mutable std::mutex m_statsMutex;
std::atomic<uint64_t> m_hits{0};
std::atomic<uint64_t> m_misses{0};
// Thread safety
mutable std::mutex m_mutex;
};
// Type alias for shared pointer
using FrameCachePtr = std::shared_ptr<FrameCache>;
} // namespace MediaPlane
#endif // FRAME_CACHE_H
@@ -0,0 +1,525 @@
#include "MayaMediaPlaneNode.h"
#include "FFmpegVideoDecoder.h"
#include "FrameCache.h"
#include <maya/MFnPluginData.h>
#include <maya/MAnimControl.h>
#include <cstring>
// Forward declarations
MStatus initializeMediaPlaneNode();
// Static member initialization
const MString MayaMediaPlaneNode::kNodeName = "MediaPlane";
const MTypeId MayaMediaPlaneNode::kNodeId = 0x0013A5F0;
const MString MayaMediaPlaneNode::kNodeClassification = "texture";
// Attribute objects
MObject MayaMediaPlaneNode::aVideoFile;
MObject MayaMediaPlaneNode::aCurrentTime;
MObject MayaMediaPlaneNode::aFrameRate;
MObject MayaMediaPlaneNode::aPlaybackRate;
MObject MayaMediaPlaneNode::aUseMayaFrameRate;
MObject MayaMediaPlaneNode::aLoop;
MObject MayaMediaPlaneNode::aPostEffectCrop;
MObject MayaMediaPlaneNode::aPostEffectResize;
MObject MayaMediaPlaneNode::aPostEffectFlip;
MObject MayaMediaPlaneNode::aCachePolicy;
MObject MayaMediaPlaneNode::aCacheSize;
MObject MayaMediaPlaneNode::aClearCache;
// Output attributes
MObject MayaMediaPlaneNode::aOutFrameData;
MObject MayaMediaPlaneNode::aOutFrameWidth;
MObject MayaMediaPlaneNode::aOutFrameHeight;
MObject MayaMediaPlaneNode::aOutFrameTimestamp;
MObject MayaMediaPlaneNode::aOutFrameCount;
MObject MayaMediaPlaneNode::aOutIsValid;
MObject MayaMediaPlaneNode::aOutCacheHitRatio;
// Constructor
MayaMediaPlaneNode::MayaMediaPlaneNode()
{
m_decoder = std::make_unique<MediaPlane::FFmpegVideoDecoder>();
m_frameCache = std::make_unique<MediaPlane::FrameCache>(m_maxCacheMemory, m_maxCacheFrames);
m_videoWidth = 0;
m_videoHeight = 0;
m_videoFrameRate = 0.0;
m_videoFrameCount = 0;
m_lastMayaFrameRate = 0.0;
m_startTime = 0.0;
}
// Destructor
MayaMediaPlaneNode::~MayaMediaPlaneNode()
{
closeVideoFile();
}
// Creator function
void* MayaMediaPlaneNode::creator()
{
return new MayaMediaPlaneNode();
}
// Initialize node attributes
MStatus MayaMediaPlaneNode::initialize()
{
MStatus status;
MFnNumericAttribute numAttr;
MFnTypedAttribute typedAttr;
// Input Attributes
aVideoFile = typedAttr.create("videoFile", "vf", MFnData::kString, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
typedAttr.setUsedAsFilename(true);
typedAttr.setStorable(true);
addAttribute(aVideoFile);
aCurrentTime = numAttr.create("currentTime", "ct", MFnNumericData::kDouble, 0.0, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(false);
numAttr.setKeyable(false);
numAttr.setReadable(false);
addAttribute(aCurrentTime);
aFrameRate = numAttr.create("frameRate", "fr", MFnNumericData::kDouble, 24.0, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(true);
numAttr.setKeyable(true);
numAttr.setMin(1.0);
numAttr.setMax(240.0);
addAttribute(aFrameRate);
aPlaybackRate = numAttr.create("playbackRate", "pr", MFnNumericData::kDouble, 1.0, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(true);
numAttr.setKeyable(true);
numAttr.setMin(0.0);
numAttr.setMax(10.0);
addAttribute(aPlaybackRate);
aUseMayaFrameRate = numAttr.create("useMayaFrameRate", "umf", MFnNumericData::kBoolean, true, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(true);
numAttr.setKeyable(true);
addAttribute(aUseMayaFrameRate);
aLoop = numAttr.create("loop", "lp", MFnNumericData::kBoolean, true, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(true);
numAttr.setKeyable(true);
addAttribute(aLoop);
aPostEffectCrop = numAttr.create("postEffectCrop", "pec", MFnNumericData::k4Double, 0.0, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(true);
numAttr.setKeyable(false);
addAttribute(aPostEffectCrop);
aPostEffectResize = numAttr.create("postEffectResize", "per", MFnNumericData::k2Double, 0.0, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(true);
numAttr.setKeyable(false);
addAttribute(aPostEffectResize);
aPostEffectFlip = numAttr.create("postEffectFlip", "pef", MFnNumericData::k2Long, 0, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(true);
numAttr.setKeyable(false);
addAttribute(aPostEffectFlip);
aCachePolicy = numAttr.create("cachePolicy", "cp", MFnNumericData::kInt, 1, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(true);
numAttr.setKeyable(false);
numAttr.setMin(0);
numAttr.setMax(2);
addAttribute(aCachePolicy);
aCacheSize = numAttr.create("cacheSize", "cs", MFnNumericData::kInt, 256, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(true);
numAttr.setKeyable(false);
numAttr.setMin(16);
numAttr.setMax(2048);
addAttribute(aCacheSize);
aClearCache = numAttr.create("clearCache", "cc", MFnNumericData::kBoolean, false, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(false);
numAttr.setKeyable(false);
addAttribute(aClearCache);
// Output Attributes
aOutFrameWidth = numAttr.create("outFrameWidth", "ofw", MFnNumericData::kInt, 0, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(false);
numAttr.setReadable(true);
addAttribute(aOutFrameWidth);
aOutFrameHeight = numAttr.create("outFrameHeight", "ofh", MFnNumericData::kInt, 0, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(false);
numAttr.setReadable(true);
addAttribute(aOutFrameHeight);
aOutFrameTimestamp = numAttr.create("outFrameTimestamp", "oft", MFnNumericData::kDouble, 0.0, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(false);
numAttr.setReadable(true);
addAttribute(aOutFrameTimestamp);
aOutFrameCount = numAttr.create("outFrameCount", "ofc", MFnNumericData::kInt64, 0, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(false);
numAttr.setReadable(true);
addAttribute(aOutFrameCount);
aOutIsValid = numAttr.create("outIsValid", "oiv", MFnNumericData::kBoolean, false, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(false);
numAttr.setReadable(true);
addAttribute(aOutIsValid);
aOutCacheHitRatio = numAttr.create("outCacheHitRatio", "och", MFnNumericData::kDouble, 0.0, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
numAttr.setStorable(false);
numAttr.setReadable(true);
addAttribute(aOutCacheHitRatio);
// Output frame validity flag (0 = no frame, 1 = frame available)
aOutFrameData = numAttr.create("outFrameData", "ofd", MFnNumericData::kInt, 0, &status);
if (!status) return status;
numAttr.setStorable(false);
numAttr.setReadable(true);
addAttribute(aOutFrameData);
return attributeAffectsSetup();
}
// Set up attribute dependencies
MStatus MayaMediaPlaneNode::attributeAffectsSetup()
{
attributeAffects(aVideoFile, aOutFrameWidth);
attributeAffects(aVideoFile, aOutFrameHeight);
attributeAffects(aVideoFile, aOutFrameTimestamp);
attributeAffects(aVideoFile, aOutFrameCount);
attributeAffects(aVideoFile, aOutIsValid);
attributeAffects(aVideoFile, aOutFrameData);
attributeAffects(aVideoFile, aOutCacheHitRatio);
attributeAffects(aCurrentTime, aOutFrameWidth);
attributeAffects(aCurrentTime, aOutFrameHeight);
attributeAffects(aCurrentTime, aOutFrameTimestamp);
attributeAffects(aCurrentTime, aOutIsValid);
attributeAffects(aCurrentTime, aOutFrameData);
attributeAffects(aFrameRate, aOutFrameTimestamp);
attributeAffects(aFrameRate, aOutFrameData);
attributeAffects(aPlaybackRate, aOutFrameTimestamp);
attributeAffects(aPlaybackRate, aOutFrameData);
attributeAffects(aUseMayaFrameRate, aOutFrameTimestamp);
attributeAffects(aUseMayaFrameRate, aOutFrameData);
attributeAffects(aLoop, aOutFrameData);
attributeAffects(aPostEffectCrop, aOutFrameData);
attributeAffects(aPostEffectResize, aOutFrameData);
attributeAffects(aPostEffectFlip, aOutFrameData);
attributeAffects(aCachePolicy, aOutCacheHitRatio);
attributeAffects(aCacheSize, aOutCacheHitRatio);
attributeAffects(aClearCache, aOutCacheHitRatio);
attributeAffects(aClearCache, aOutFrameData);
return MS::kSuccess;
}
void MayaMediaPlaneNode::postConstructor() {}
MStatus MayaMediaPlaneNode::destroy()
{
closeVideoFile();
return MS::kSuccess;
}
// Compute method
MStatus MayaMediaPlaneNode::compute(const MPlug& plug, MDataBlock& dataBlock)
{
if (plug == aOutFrameData || plug == aOutFrameWidth || plug == aOutFrameHeight ||
plug == aOutFrameTimestamp || plug == aOutFrameCount || plug == aOutIsValid ||
plug == aOutCacheHitRatio) {
MStatus status = computeVideoFrame(dataBlock);
if (!status) {
MDataHandle outIsValid = dataBlock.outputValue(aOutIsValid);
outIsValid.set(false);
dataBlock.setClean(plug);
return status;
}
}
return MS::kSuccess;
}
// Compute video frame
MStatus MayaMediaPlaneNode::computeVideoFrame(MDataBlock& dataBlock)
{
MStatus status;
MDataHandle videoFileHandle = dataBlock.inputValue(aVideoFile, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
MString videoFile = videoFileHandle.asString();
MDataHandle currentTimeHandle = dataBlock.inputValue(aCurrentTime, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
double currentTime = currentTimeHandle.asDouble();
MDataHandle frameRateHandle = dataBlock.inputValue(aFrameRate, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
double frameRate = frameRateHandle.asDouble();
MDataHandle playbackRateHandle = dataBlock.inputValue(aPlaybackRate, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
double playbackRate = playbackRateHandle.asDouble();
MDataHandle useMayaFrameRateHandle = dataBlock.inputValue(aUseMayaFrameRate, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
bool useMayaFrameRate = useMayaFrameRateHandle.asBool();
MDataHandle loopHandle = dataBlock.inputValue(aLoop, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
bool loop = loopHandle.asBool();
MDataHandle cacheSizeHandle = dataBlock.inputValue(aCacheSize, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
int cacheSizeMB = cacheSizeHandle.asInt();
MDataHandle clearCacheHandle = dataBlock.inputValue(aClearCache, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
bool clearCache = clearCacheHandle.asBool();
// Frame Rate Synchronization
double mayaFrameRate = getMayaFrameRate();
if (useMayaFrameRate) {
if (mayaFrameRate != m_lastMayaFrameRate) {
m_lastMayaFrameRate = mayaFrameRate;
frameRate = mayaFrameRate;
} else if (frameRate == 0.0) {
frameRate = mayaFrameRate;
}
} else {
frameRate = m_videoFrameRate > 0 ? m_videoFrameRate : frameRate;
}
if (playbackRate < 0.25) playbackRate = 0.25;
if (playbackRate > 4.0) playbackRate = 4.0;
if (clearCache) {
std::lock_guard<std::mutex> lock(m_cacheMutex);
m_frameCache->clear();
MDataHandle clearCacheOut = dataBlock.outputValue(aClearCache, &status);
clearCacheOut.set(false);
}
if (cacheSizeMB * 1024 * 1024 != m_maxCacheMemory) {
m_maxCacheMemory = cacheSizeMB * 1024 * 1024;
m_frameCache->setMaxMemorySize(m_maxCacheMemory);
}
if (videoFile.length() > 0) {
if (videoFile != m_currentVideoFile || !m_decoder->isOpen()) {
status = openVideoFile(videoFile);
if (!status) {
MGlobal::displayError("Failed to open video file");
return status;
}
}
}
if (!m_decoder->isOpen()) {
MDataHandle outIsValid = dataBlock.outputValue(aOutIsValid, &status);
outIsValid.set(false);
MDataHandle outFrameWidth = dataBlock.outputValue(aOutFrameWidth, &status);
outFrameWidth.set(0);
MDataHandle outFrameHeight = dataBlock.outputValue(aOutFrameHeight, &status);
outFrameHeight.set(0);
return MS::kSuccess;
}
double effectiveFrameRate = useMayaFrameRate ? frameRate : m_videoFrameRate;
int64_t targetFrame = calculateTargetFrame(currentTime, effectiveFrameRate, playbackRate, loop);
if (!isValidFrameIndex(targetFrame)) {
MDataHandle outIsValid = dataBlock.outputValue(aOutIsValid, &status);
outIsValid.set(false);
return MS::kSuccess;
}
// Get frame
MediaPlane::FFmpegVideoDecoder::FrameData frameData = m_decoder->getFrame(targetFrame);
if (frameData.data) {
MDataHandle outTimestamp = dataBlock.outputValue(aOutFrameTimestamp, &status);
outTimestamp.set(frameData.timestamp);
}
// Apply post-effects
status = applyPostEffects(dataBlock);
CHECK_MSTATUS_AND_RETURN_IT(status);
MDataHandle outIsValid = dataBlock.outputValue(aOutIsValid, &status);
outIsValid.set(frameData.data != nullptr);
MDataHandle outFrameWidth = dataBlock.outputValue(aOutFrameWidth, &status);
outFrameWidth.set(m_videoWidth);
MDataHandle outFrameHeight = dataBlock.outputValue(aOutFrameHeight, &status);
outFrameHeight.set(m_videoHeight);
MDataHandle outFrameCount = dataBlock.outputValue(aOutFrameCount, &status);
outFrameCount.setInt64(m_videoFrameCount);
return updateCacheStats(dataBlock);
}
// Open video file
MStatus MayaMediaPlaneNode::openVideoFile(const MString& filePath)
{
MStatus status;
std::lock_guard<std::mutex> lock(m_decoderMutex);
closeVideoFile();
m_currentVideoFile = filePath;
std::string path = filePath.asUTF8();
if (!m_decoder->open(path)) {
MGlobal::displayError(MString("Failed to open video: ") + m_decoder->getLastError().c_str());
return MS::kFailure;
}
const auto& videoInfo = m_decoder->getVideoInfo();
m_videoWidth = videoInfo.width;
m_videoHeight = videoInfo.height;
m_videoFrameRate = videoInfo.frameRate;
m_videoFrameCount = videoInfo.frameCount;
m_frameCache->invalidate();
MGlobal::displayInfo("Opened video");
return MS::kSuccess;
}
// Close video file
void MayaMediaPlaneNode::closeVideoFile()
{
std::lock_guard<std::mutex> lock(m_decoderMutex);
if (m_decoder && m_decoder->isOpen()) {
m_decoder->close();
}
m_currentVideoFile = "";
m_videoWidth = 0;
m_videoHeight = 0;
m_videoFrameRate = 0.0;
m_videoFrameCount = 0;
}
// Calculate target frame
int64_t MayaMediaPlaneNode::calculateTargetFrame(double currentTime, double frameRate, double playbackRate, bool loop)
{
if (frameRate <= 0 || playbackRate <= 0) return 0;
double effectiveFrameRate = frameRate * playbackRate;
int64_t frame = static_cast<int64_t>(currentTime * effectiveFrameRate);
if (loop && m_videoFrameCount > 0) frame = frame % m_videoFrameCount;
if (frame < 0) frame = 0;
else if (!loop && frame >= m_videoFrameCount) frame = m_videoFrameCount - 1;
return frame;
}
MStatus MayaMediaPlaneNode::getFrame(int64_t frameIndex, MDataHandle& frameDataHandle) { return MS::kSuccess; }
MStatus MayaMediaPlaneNode::applyPostEffects(MDataBlock& dataBlock)
{
MStatus status;
MDataHandle cropHandle = dataBlock.inputValue(aPostEffectCrop, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
double cropX = 0, cropY = 0, cropW = 0, cropH = 0;
double* cropData = cropHandle.asDouble4();
if (cropData) { cropX = cropData[0]; cropY = cropData[1]; cropW = cropData[2]; cropH = cropData[3]; }
MDataHandle resizeHandle = dataBlock.inputValue(aPostEffectResize, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
double resizeW = 0, resizeH = 0;
double* resizeData = resizeHandle.asDouble2();
if (resizeData) { resizeW = resizeData[0]; resizeH = resizeData[1]; }
MDataHandle flipHandle = dataBlock.inputValue(aPostEffectFlip, &status);
CHECK_MSTATUS_AND_RETURN_IT(status);
int flipInt = flipHandle.asInt();
bool flipH = (flipInt & 1) != 0;
bool flipV = (flipInt & 2) != 0;
bool postEffectsChanged = (cropX != m_lastCropX || cropY != m_lastCropY ||
cropW != m_lastCropW || cropH != m_lastCropH ||
resizeW != m_lastResizeW || resizeH != m_lastResizeH ||
flipH != m_lastFlipH || flipV != m_lastFlipV);
if (postEffectsChanged) {
m_lastCropX = cropX; m_lastCropY = cropY; m_lastCropW = cropW; m_lastCropH = cropH;
m_lastResizeW = resizeW; m_lastResizeH = resizeH;
m_lastFlipH = flipH; m_lastFlipV = flipV;
std::lock_guard<std::mutex> lock(m_cacheMutex);
m_frameCache->invalidate();
}
return MS::kSuccess;
}
MStatus MayaMediaPlaneNode::updateCacheStats(MDataBlock& dataBlock)
{
MStatus status;
auto stats = m_frameCache->getStats();
MDataHandle outCacheHitRatio = dataBlock.outputValue(aOutCacheHitRatio, &status);
outCacheHitRatio.set(stats.hitRatio);
return MS::kSuccess;
}
bool MayaMediaPlaneNode::isValidFrameIndex(int64_t index) const
{
return index >= 0 && index < m_videoFrameCount;
}
double MayaMediaPlaneNode::getMayaFrameRate() const
{
MTime time = MAnimControl::currentTime();
MTime::Unit timeUnit = time.uiUnit();
double fps = 24.0;
switch (timeUnit) {
case MTime::kHours: fps = 3600.0; break;
case MTime::kMinutes: fps = 60.0; break;
case MTime::kSeconds: fps = 1.0; break;
case MTime::kMilliseconds: fps = 1000.0; break;
case MTime::kGames: fps = 15.0; break;
case MTime::kFilm: fps = 24.0; break;
case MTime::kNTSCFrame: fps = 30.0; break;
case MTime::kNTSCField: fps = 60.0; break;
case MTime::kPALFrame: fps = 25.0; break;
case MTime::kPALField: fps = 50.0; break;
case MTime::kShowScan: fps = 48.0; break;
default: fps = 24.0; break;
}
return fps;
}
void MayaMediaPlaneNode::processPostEffects(FrameDataWrapper& frame,
double cropX, double cropY, double cropW, double cropH,
double resizeW, double resizeH, bool flipH, bool flipV)
{
if (!frame.data || frame.width <= 0 || frame.height <= 0) return;
// Simplified post-effects processing would go here
}
+218
View File
@@ -0,0 +1,218 @@
#ifndef MAYA_MEDIA_PLANE_NODE_H
#define MAYA_MEDIA_PLANE_NODE_H
// Maya API headers
#include <maya/MObject.h>
#include <maya/MPxNode.h>
#include <maya/MString.h>
#include <maya/MTypeId.h>
#include <maya/MPlug.h>
#include <maya/MDataBlock.h>
#include <maya/MDataHandle.h>
#include <maya/MArrayDataHandle.h>
#include <maya/MStatus.h>
#include <maya/MGlobal.h>
#include <maya/MFnTypedAttribute.h>
#include <maya/MFnNumericAttribute.h>
#include <maya/MFnMessageAttribute.h>
#include <maya/MMatrix.h>
#include <maya/MTime.h>
// MFnPlugin - included in Plugin.cpp only, not needed in header
#include <maya/MAnimControl.h>
#include <string>
#include <memory>
#include <mutex>
// Forward declarations
namespace MediaPlane {
class FFmpegVideoDecoder;
class FrameCache;
}
// Frame data structure for Maya attribute - defined before the class
struct FrameDataWrapper {
uint8_t* data = nullptr;
int width = 0;
int height = 0;
int lineSize = 0;
double timestamp = 0.0;
int64_t frameIndex = 0;
FrameDataWrapper() = default;
~FrameDataWrapper() {
if (data) {
delete[] data;
data = nullptr;
}
}
// Non-copyable
FrameDataWrapper(const FrameDataWrapper&) = delete;
FrameDataWrapper& operator=(const FrameDataWrapper&) = delete;
// Movable
FrameDataWrapper(FrameDataWrapper&& other) noexcept
: data(other.data)
, width(other.width)
, height(other.height)
, lineSize(other.lineSize)
, timestamp(other.timestamp)
, frameIndex(other.frameIndex)
{
other.data = nullptr;
other.width = 0;
other.height = 0;
}
FrameDataWrapper& operator=(FrameDataWrapper&& other) noexcept {
if (this != &other) {
if (data) {
delete[] data;
}
data = other.data;
width = other.width;
height = other.height;
lineSize = other.lineSize;
timestamp = other.timestamp;
frameIndex = other.frameIndex;
other.data = nullptr;
other.width = 0;
other.height = 0;
}
return *this;
}
};
/**
* MayaMediaPlaneNode - Maya MPxNode for video playback on image planes
*
* This node provides:
* - Video file input (MP4 support via FFmpeg)
* - Frame-by-frame access synchronized with Maya's timeline
* - Configurable playback rate and loop behavior
* - Post-effects (crop, resize, flip)
* - Frame caching for smooth playback
*/
class MayaMediaPlaneNode : public MPxNode {
public:
// Node type name and ID
static const MString kNodeName;
static const MTypeId kNodeId;
// Node classification for Maya
static const MString kNodeClassification;
// Constructor / Destructor
MayaMediaPlaneNode();
virtual ~MayaMediaPlaneNode();
// Maya API overrides
virtual MStatus compute(const MPlug& plug, MDataBlock& dataBlock) override;
MStatus initialize();
void postConstructor();
MStatus destroy();
// Attribute affect setup
static MStatus attributeAffectsSetup();
// Creator function for Maya plugin system
static void* creator();
// Plugin initialization
static MStatus initializePlugin(MObject obj);
static MStatus uninitializePlugin(MObject obj);
private:
// Attribute IDs
static MObject aVideoFile;
static MObject aCurrentTime;
static MObject aFrameRate;
static MObject aPlaybackRate;
static MObject aUseMayaFrameRate;
static MObject aLoop;
static MObject aPostEffectCrop;
static MObject aPostEffectResize;
static MObject aPostEffectFlip;
static MObject aCachePolicy;
static MObject aCacheSize;
static MObject aClearCache;
// Output attributes
static MObject aOutFrameData;
static MObject aOutFrameWidth;
static MObject aOutFrameHeight;
static MObject aOutFrameTimestamp;
static MObject aOutFrameCount;
static MObject aOutIsValid;
static MObject aOutCacheHitRatio;
private:
// Compute helper methods
MStatus computeVideoFrame(MDataBlock& dataBlock);
MStatus computeOutputAttributes(MDataBlock& dataBlock, const MDataHandle& frameDataHandle);
// Video file handling
MStatus openVideoFile(const MString& filePath);
void closeVideoFile();
// Frame calculation
int64_t calculateTargetFrame(double currentTime, double frameRate, double playbackRate, bool loop);
// Frame retrieval
MStatus getFrame(int64_t frameIndex, MDataHandle& frameDataHandle);
// Post-effects
MStatus applyPostEffects(MDataBlock& dataBlock);
// Cache management
MStatus updateCacheStats(MDataBlock& dataBlock);
// Validate frame index
bool isValidFrameIndex(int64_t index) const;
// Get Maya's current frame rate from the timeline
double getMayaFrameRate() const;
// Process frame with post-effects
void processPostEffects(FrameDataWrapper& frame,
double cropX, double cropY, double cropW, double cropH,
double resizeW, double resizeH,
bool flipH, bool flipV);
private:
// FFmpeg decoder
std::unique_ptr<MediaPlane::FFmpegVideoDecoder> m_decoder;
// Frame cache
std::unique_ptr<MediaPlane::FrameCache> m_frameCache;
// Current video file path
MString m_currentVideoFile;
// Cached video info
int m_videoWidth = 0;
int m_videoHeight = 0;
double m_videoFrameRate = 0.0;
int64_t m_videoFrameCount = 0;
// Thread safety
std::mutex m_decoderMutex;
std::mutex m_cacheMutex;
// Cache settings
size_t m_maxCacheMemory = 256 * 1024 * 1024; // 256MB default
size_t m_maxCacheFrames = 100;
// Frame rate tracking
double m_lastMayaFrameRate = 0.0;
double m_startTime = 0.0;
// Post-effect parameters
double m_lastCropX = 0, m_lastCropY = 0, m_lastCropW = 0, m_lastCropH = 0;
double m_lastResizeW = 0, m_lastResizeH = 0;
bool m_lastFlipH = false, m_lastFlipV = false;
};
#endif // MAYA_MEDIA_PLANE_NODE_H
+19
View File
@@ -0,0 +1,19 @@
+ MAYAVERSION:2022 MediaPlane 1.0.0 .
PATH+:=bin
plug-ins: plug-ins/Maya2022
+ MAYAVERSION:2023 MediaPlane 1.0.0 .
PATH+:=bin
plug-ins: plug-ins/Maya2023
+ MAYAVERSION:2024 MediaPlane 1.0.0 .
PATH+:=bin
plug-ins: plug-ins/Maya2024
+ MAYAVERSION:2025 MediaPlane 1.0.0 .
PATH+:=bin
plug-ins: plug-ins/Maya2025
+ MAYAVERSION:2026 MediaPlane 1.0.0 .
PATH+:=bin
plug-ins: plug-ins/Maya2026
+53
View File
@@ -0,0 +1,53 @@
// Plugin.cpp
// Maya Plugin entry point for MayaMediaPlaneNode
// This file contains the plugin initialization and uninitialization functions
#include "MayaMediaPlaneNode.h"
#include <maya/MFnPlugin.h>
#include <maya/MGlobal.h>
// Forward declaration
MStatus initializeMediaPlaneNode();
// Plugin initialization - non-member function required by Maya
MStatus initializePlugin(MObject obj)
{
MStatus status;
MFnPlugin plugin(obj, "MediaPlane", "1.0", "Any", &status);
if (!status) return status;
status = plugin.registerNode("MediaPlane",
MayaMediaPlaneNode::kNodeId,
MayaMediaPlaneNode::creator,
initializeMediaPlaneNode,
MPxNode::kDependNode,
&MayaMediaPlaneNode::kNodeClassification);
if (!status) return status;
// Display info about AE Template
MGlobal::displayInfo("MayaMediaPlaneNode plugin loaded");
MGlobal::displayInfo("For Attribute Editor customization, source: source \"AETemplate_MediaPlane.mel\"");
return MS::kSuccess;
}
// Plugin uninitialization - non-member function required by Maya
MStatus uninitializePlugin(MObject obj)
{
MStatus status;
MFnPlugin plugin(obj);
if (!status) return status;
status = plugin.deregisterNode(MayaMediaPlaneNode::kNodeId);
if (!status) return status;
MGlobal::displayInfo("MayaMediaPlaneNode plugin unloaded");
return MS::kSuccess;
}
// Non-member initialize function for Maya node attributes registration
MStatus initializeMediaPlaneNode()
{
MayaMediaPlaneNode node;
return node.initialize();
}