This commit is contained in:
2025-10-15 04:40:21 +09:00
parent dfa944a789
commit 5198750b31
4 changed files with 140 additions and 34 deletions

View File

@@ -182,6 +182,10 @@ void VavCoreVulkanBridge::CloseVideoFile() {
m_currentPositionUs = 0;
m_frameNumber = 0;
// Reset drain mode state
m_isDraining = false;
m_drainedFrameCount = 0;
LOGI("Video file closed");
}
@@ -232,6 +236,10 @@ bool VavCoreVulkanBridge::Stop() {
m_currentPositionUs = 0;
m_frameNumber = 0;
// Reset drain mode state
m_isDraining = false;
m_drainedFrameCount = 0;
if (m_player) {
vavcore_reset(m_player);
}
@@ -271,16 +279,32 @@ bool VavCoreVulkanBridge::ProcessNextFrame() {
}
// Decode next frame to Vulkan surface (GPU zero-copy pipeline)
// 16-Frame Buffering Pattern:
// - Normal mode: target_surface=non-NULL → read packet and decode
// - Drain mode: target_surface=NULL → flush buffered frames (no packet reading)
VavCoreVideoFrame frame = {};
LOGI("Calling vavcore_decode_to_surface...");
LOGI("Calling vavcore_decode_to_surface (draining=%s)...", m_isDraining ? "true" : "false");
// Use target_surface to signal drain mode to VavCore
// Normal mode: Pass dummy non-NULL pointer (VkImage is managed internally by VavCore)
// Drain mode: Pass NULL to trigger buffered frame flush
void* target_surface = m_isDraining ? nullptr : (void*)0x1;
VavCoreResult result = vavcore_decode_to_surface(m_player,
VAVCORE_SURFACE_VULKAN_IMAGE,
nullptr, // target_surface (not needed for Vulkan)
target_surface,
&frame);
LOGI("vavcore_decode_to_surface returned: %d", result);
if (result == VAVCORE_END_OF_STREAM) {
LOGI("End of stream reached");
// Handle 16-Frame Buffering Pattern results
if (result == VAVCORE_PACKET_ACCEPTED) {
// Priming phase: packet accepted but no frame output yet
// This is normal during the first 16 frames (buffering phase)
LOGI("Packet accepted - buffering phase (no frame output yet)");
return true; // Continue processing, not an error
} else if (result == VAVCORE_END_OF_STREAM) {
// All buffered frames consumed - draining complete
LOGI("End of stream reached - all buffered frames consumed");
SetPlaybackState(PlaybackState::STOPPED);
return false;
} else if (result != VAVCORE_SUCCESS) {
@@ -740,9 +764,12 @@ void VavCoreVulkanBridge::PlaybackThreadMain() {
LOGI("Playback thread started");
int frameCount = 0;
const uint32_t MAX_DRAIN_ATTEMPTS = 16; // Maximum buffered frames
while (ShouldContinuePlayback()) {
frameCount++;
LOGI("=== Playback Loop Iteration #%d START ===", frameCount);
LOGI("=== Playback Loop Iteration #%d START (draining=%s) ===",
frameCount, m_isDraining ? "true" : "false");
auto frameStart = std::chrono::steady_clock::now();
// Process next frame
@@ -751,10 +778,32 @@ void VavCoreVulkanBridge::PlaybackThreadMain() {
LOGI("ProcessNextFrame() returned: %s", success ? "true" : "false");
if (!success) {
LOGI("End of video or decode error, stopping playback");
// Set state to stopped and break the loop
SetPlaybackState(PlaybackState::STOPPED);
break;
// Check if we should enter drain mode
if (!m_isDraining) {
LOGI("End of file detected - entering drain mode to flush buffered frames");
m_isDraining = true;
m_drainedFrameCount = 0;
// Continue to drain buffered frames
continue;
} else {
// Already draining and got failure - all frames consumed
LOGI("Drain complete - all buffered frames consumed");
SetPlaybackState(PlaybackState::STOPPED);
break;
}
}
// Check drain attempt limit
if (m_isDraining) {
m_drainedFrameCount++;
LOGI("Drained frame %u/%u", m_drainedFrameCount, MAX_DRAIN_ATTEMPTS);
if (m_drainedFrameCount >= MAX_DRAIN_ATTEMPTS) {
LOGI("Maximum drain attempts reached (%u frames)", MAX_DRAIN_ATTEMPTS);
SetPlaybackState(PlaybackState::STOPPED);
break;
}
}
// Calculate frame timing

View File

@@ -165,6 +165,10 @@ private:
uint64_t m_renderedFrameCount = 0;
uint64_t m_droppedFrameCount = 0;
// 16-Frame Buffering Pattern support (MediaCodec latency hiding)
bool m_isDraining = false; // True when draining buffered frames (EOS reached)
uint32_t m_drainedFrameCount = 0; // Number of frames drained from buffer
// Continuous playback thread
std::thread m_playbackThread;
std::atomic<bool> m_shouldContinuePlayback{false};

View File

@@ -56,6 +56,9 @@ void MediaCodecAsyncHandler::Cleanup() {
while (!m_async_input_buffer_queue.empty()) {
m_async_input_buffer_queue.pop();
}
while (!m_pending_output_buffers.empty()) {
m_pending_output_buffers.pop();
}
// Reset hidden queue pattern state
m_prebuffering = true;
@@ -101,33 +104,30 @@ bool MediaCodecAsyncHandler::InitializeAsyncMode() {
};
m_async_callbacks.onOutputBufferAvailable = [this](int32_t index, AMediaCodecBufferInfo* bufferInfo) {
// Output buffer available - process in callback
// DEADLOCK FIX: Do NOT call MediaCodec APIs from callback thread
// Instead, store index and bufferInfo in queue for processing by decode thread
try {
VideoFrame frame;
if (ProcessAsyncOutputFrame(index, bufferInfo, frame)) {
std::lock_guard<std::mutex> lock(m_async_mutex);
std::lock_guard<std::mutex> lock(m_async_mutex);
// Hidden Queue Pattern: Check buffer size limit to prevent overflow
if (m_async_output_queue.size() >= MAX_BUFFER_SIZE) {
LogWarning("Frame queue full (size=" + std::to_string(m_async_output_queue.size()) +
"/" + std::to_string(MAX_BUFFER_SIZE) + ") - dropping frame (timestamp=" +
std::to_string(bufferInfo->presentationTimeUs) + "us)");
// Frame resources already released by ProcessAsyncOutputFrame
// This prevents unbounded queue growth when consumer is slower than producer
return;
}
// Copy bufferInfo (callback pointer is ephemeral and will be invalidated)
PendingOutputBuffer pending;
pending.index = index;
pending.bufferInfo = *bufferInfo; // Deep copy
AsyncFrameData async_data;
async_data.frame = std::make_unique<VideoFrame>(std::move(frame));
async_data.timestamp_us = bufferInfo->presentationTimeUs;
// TODO: NDK 26 does not expose keyframe flag in AMediaCodecBufferInfo
// Keyframe detection needs to be done via other means (e.g., frame analysis)
async_data.is_keyframe = false; // Placeholder - keyframe flag not available in NDK 26
async_data.decode_start_time = std::chrono::steady_clock::now();
m_async_output_queue.push(std::move(async_data));
m_async_condition.notify_one();
// Check queue size limit to prevent overflow
if (m_pending_output_buffers.size() >= MAX_BUFFER_SIZE) {
LogWarning("Pending output buffer queue full (size=" + std::to_string(m_pending_output_buffers.size()) +
"/" + std::to_string(MAX_BUFFER_SIZE) + ") - dropping buffer (index=" +
std::to_string(index) + ", timestamp=" + std::to_string(bufferInfo->presentationTimeUs) + "us)");
// Release buffer immediately without rendering to prevent MediaCodec stall
AMediaCodec_releaseOutputBuffer(m_codec, index, false);
return;
}
m_pending_output_buffers.push(pending);
LogInfo("Output buffer stored in pending queue: index=" + std::to_string(index) +
", pending queue size=" + std::to_string(m_pending_output_buffers.size()));
m_async_condition.notify_one();
} catch (const std::exception& e) {
LogError("Exception in onOutputBufferAvailable: " + std::string(e.what()));
} catch (...) {
@@ -218,6 +218,9 @@ void MediaCodecAsyncHandler::CleanupAsyncMode() {
while (!m_async_input_buffer_queue.empty()) {
m_async_input_buffer_queue.pop();
}
while (!m_pending_output_buffers.empty()) {
m_pending_output_buffers.pop();
}
LogInfo("Async mode cleanup complete");
}
@@ -310,10 +313,52 @@ bool MediaCodecAsyncHandler::DecodeFrameAsync(const uint8_t* packet_data, size_t
return false;
}
LogInfo("DecodeFrameAsync: Input buffer queued successfully");
LogInfo("DecodeFrameAsync: Input buffer queued successfully, now processing pending outputs...");
// DEADLOCK FIX: Process pending output buffers (defer MediaCodec API calls out of callback)
// Callbacks store output indices in pending queue, decode thread processes them here
{
std::unique_lock<std::mutex> lock(m_async_mutex);
// Process all pending output buffers
while (!m_pending_output_buffers.empty()) {
PendingOutputBuffer pending = m_pending_output_buffers.front();
m_pending_output_buffers.pop();
lock.unlock(); // Release lock while calling MediaCodec APIs
LogInfo("DecodeFrameAsync: Processing pending output buffer index=" + std::to_string(pending.index));
// Process frame outside callback context (safe to call releaseOutputBuffer here)
VideoFrame frame;
if (ProcessAsyncOutputFrame(pending.index, &pending.bufferInfo, frame)) {
// Frame processed successfully - add to output queue
std::lock_guard<std::mutex> queue_lock(m_async_mutex);
// Hidden Queue Pattern: Check buffer size limit
if (m_async_output_queue.size() >= MAX_BUFFER_SIZE) {
LogWarning("Frame queue full (size=" + std::to_string(m_async_output_queue.size()) +
"/" + std::to_string(MAX_BUFFER_SIZE) + ") - dropping frame");
// Frame resources already released by ProcessAsyncOutputFrame
lock.lock();
continue;
}
AsyncFrameData async_data;
async_data.frame = std::make_unique<VideoFrame>(std::move(frame));
async_data.timestamp_us = pending.bufferInfo.presentationTimeUs;
async_data.is_keyframe = false;
async_data.decode_start_time = std::chrono::steady_clock::now();
m_async_output_queue.push(std::move(async_data));
LogInfo("DecodeFrameAsync: Frame added to output queue (size=" +
std::to_string(m_async_output_queue.size()) + ")");
}
lock.lock(); // Re-acquire lock for next iteration
}
}
// Check if output frame is already available in queue (non-blocking)
// MediaCodec async callbacks will populate the queue when frames are ready
{
std::lock_guard<std::mutex> lock(m_async_mutex);

View File

@@ -118,6 +118,14 @@ private:
// Async input buffer index queue
std::queue<int32_t> m_async_input_buffer_queue;
// Pending output buffer indices (stored by callback, processed by decode thread)
// This avoids deadlock by deferring MediaCodec API calls out of callback context
struct PendingOutputBuffer {
int32_t index;
AMediaCodecBufferInfo bufferInfo; // Copy of buffer info (callback pointer is ephemeral)
};
std::queue<PendingOutputBuffer> m_pending_output_buffers;
// Async callbacks
MediaCodecAsyncCallbacks m_async_callbacks;