Modify src structure
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user