2025-08-27 15:09:05 +09:00
|
|
|
#include "Renderer.h"
|
|
|
|
|
#include <android/log.h>
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <thread>
|
2025-08-28 15:13:40 +09:00
|
|
|
#include <chrono>
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
extern void callNextMediaCallback();
|
2025-08-27 15:09:05 +09:00
|
|
|
|
|
|
|
|
#define LOG_TAG "Renderer"
|
|
|
|
|
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
|
|
|
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
2025-08-28 15:13:40 +09:00
|
|
|
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
|
2025-08-27 15:09:05 +09:00
|
|
|
Renderer::Renderer() {
|
2025-08-28 15:13:40 +09:00
|
|
|
randomEngine_.seed(std::chrono::high_resolution_clock::now().time_since_epoch().count());
|
2025-08-27 15:09:05 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Renderer::~Renderer() {
|
|
|
|
|
release();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::release() {
|
|
|
|
|
std::lock_guard<std::mutex> lock(renderMutex_);
|
|
|
|
|
currentMedia_.release();
|
|
|
|
|
nextMedia_.release();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::setNextMedia(int fd) {
|
2025-08-28 15:13:40 +09:00
|
|
|
preloader_.startNextPreload(fd);
|
2025-08-27 15:09:05 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
void Renderer::setAnimationSpeed(float speed) {
|
|
|
|
|
animationSpeed_ = speed;
|
|
|
|
|
}
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
void Renderer::setFadeDuration(int durationMs) {
|
|
|
|
|
fadeDurationMs_ = durationMs > 0 ? durationMs : 3000;
|
|
|
|
|
}
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
// --- ⬇️ 새로운 setPageTurnDelay 함수 구현 ⬇️ ---
|
|
|
|
|
void Renderer::setPageTurnDelay(int delayMs) {
|
|
|
|
|
pageTurnDelayMs_ = delayMs > 0 ? delayMs : 5000; // 0 이하면 기본값 5초
|
|
|
|
|
LOGI("PageTurn delay set to %d ms", (int)pageTurnDelayMs_);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::setAnimationMode(int mode) {
|
|
|
|
|
configuredAnimationMode_ = (mode >= 0 && mode <= static_cast<int>(AnimationMode::PAGE_TURN))
|
|
|
|
|
? static_cast<AnimationMode>(mode)
|
|
|
|
|
: AnimationMode::PAN;
|
|
|
|
|
|
|
|
|
|
determineActiveAnimationMode();
|
|
|
|
|
|
|
|
|
|
animationCycleComplete_ = false;
|
|
|
|
|
currentOffsetX_ = 0.0f;
|
|
|
|
|
currentOffsetY_ = 0.0f;
|
|
|
|
|
currentZoom_ = 1.0f;
|
|
|
|
|
xDirection_ = 1;
|
|
|
|
|
yDirection_ = 1;
|
|
|
|
|
zoomDirection_ = 1;
|
|
|
|
|
// isTransitioning_ = false; // <-- 삭제
|
|
|
|
|
isFading_ = false;
|
|
|
|
|
// transitionProgress_ = 0.0f; // <-- 삭제
|
|
|
|
|
LOGI("Animation mode changed to %d", static_cast<int>(configuredAnimationMode_));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::determineActiveAnimationMode() {
|
|
|
|
|
if (configuredAnimationMode_ == AnimationMode::RANDOM) {
|
|
|
|
|
static const std::vector<AnimationMode> availableModes = {
|
|
|
|
|
AnimationMode::PAN,
|
|
|
|
|
AnimationMode::ZOOM,
|
|
|
|
|
AnimationMode::NONE,
|
|
|
|
|
AnimationMode::PAN_ONE_WAY,
|
|
|
|
|
AnimationMode::PAGE_TURN
|
|
|
|
|
};
|
|
|
|
|
std::uniform_int_distribution<size_t> dist(0, availableModes.size() - 1);
|
|
|
|
|
activeAnimationMode_ = availableModes[dist(randomEngine_)];
|
|
|
|
|
LOGI("Random mode active: Chose animation %d", static_cast<int>(activeAnimationMode_));
|
|
|
|
|
} else {
|
|
|
|
|
activeAnimationMode_ = configuredAnimationMode_;
|
2025-08-27 15:09:05 +09:00
|
|
|
}
|
2025-08-28 15:13:40 +09:00
|
|
|
}
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
void Renderer::calculateFitScaleAndOffset(const MediaAsset& media, int surfaceWidth, int surfaceHeight,
|
|
|
|
|
float& outScale, float& outOffsetX, float& outOffsetY) const {
|
|
|
|
|
if (!media.isValid() || media.getWidth() == 0 || media.getHeight() == 0 || surfaceWidth == 0 || surfaceHeight == 0) {
|
|
|
|
|
outScale = 1.0f; outOffsetX = 0.0f; outOffsetY = 0.0f;
|
2025-08-27 15:09:05 +09:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
float mediaAspect = static_cast<float>(media.getWidth()) / media.getHeight();
|
|
|
|
|
float surfaceAspect = static_cast<float>(surfaceWidth) / surfaceHeight;
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
if (mediaAspect > surfaceAspect) {
|
|
|
|
|
outScale = static_cast<float>(surfaceHeight) / media.getHeight();
|
|
|
|
|
outOffsetX = (static_cast<float>(surfaceWidth) - (media.getWidth() * outScale)) / 2.0f;
|
|
|
|
|
outOffsetY = 0.0f;
|
|
|
|
|
} else {
|
|
|
|
|
outScale = static_cast<float>(surfaceWidth) / media.getWidth();
|
|
|
|
|
outOffsetX = 0.0f;
|
|
|
|
|
outOffsetY = (static_cast<float>(surfaceHeight) - (media.getHeight() * outScale)) / 2.0f;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
std::string Renderer::getDebugInfo() const {
|
|
|
|
|
std::stringstream ss;
|
|
|
|
|
ss << "========== Native State ==========\n";
|
|
|
|
|
ss << " Configured Mode: " << static_cast<int>(configuredAnimationMode_) << "\n";
|
|
|
|
|
ss << " Active Mode : " << static_cast<int>(activeAnimationMode_) << "\n";
|
|
|
|
|
ss << " Fading: " << (isFading_ ? "YES" : "NO") << "\n";
|
|
|
|
|
// ss << " Transitioning: " << (isTransitioning_ ? "YES" : "NO") << "\n"; // <-- 삭제
|
|
|
|
|
ss << " Anim Complete: " << (animationCycleComplete_ ? "YES" : "NO") << "\n";
|
|
|
|
|
ss << " Offset (X, Y): (" << std::fixed << std::setprecision(2) << currentOffsetX_ << ", " << currentOffsetY_ << ")\n";
|
|
|
|
|
ss << " Zoom: " << std::fixed << std::setprecision(2) << currentZoom_ << "\n";
|
|
|
|
|
ss << " PageTurnDelay: " << pageTurnDelayMs_ << " ms\n"; // <-- 추가
|
|
|
|
|
|
|
|
|
|
if (currentMedia_.isValid()) {
|
|
|
|
|
ss << " Current Media: VALID ["
|
|
|
|
|
<< (currentMedia_.getType() == MediaAsset::Type::IMAGE ? "Image" : "Video")
|
|
|
|
|
<< " " << currentMedia_.getWidth() << "x" << currentMedia_.getHeight() << "]\n";
|
|
|
|
|
} else {
|
|
|
|
|
ss << " Current Media: INVALID\n";
|
2025-08-27 15:09:05 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
if (nextMedia_.isValid()) {
|
|
|
|
|
ss << " Next Media : VALID ["
|
|
|
|
|
<< (nextMedia_.getType() == MediaAsset::Type::IMAGE ? "Image" : "Video")
|
|
|
|
|
<< " " << nextMedia_.getWidth() << "x" << nextMedia_.getHeight() << "]";
|
|
|
|
|
} else {
|
|
|
|
|
ss << " Next Media : INVALID";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ss.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::renderFrame(ANativeWindow* window) {
|
|
|
|
|
if (!window) return;
|
|
|
|
|
int surfaceWidth = ANativeWindow_getWidth(window);
|
|
|
|
|
int surfaceHeight = ANativeWindow_getHeight(window);
|
|
|
|
|
std::lock_guard<std::mutex> lock(renderMutex_);
|
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
|
|
|
|
|
|
// 1. 현재 미디어 로딩 (초기 또는 페이드/전환 완료 후)
|
|
|
|
|
if (!currentMedia_.isValid()) {
|
|
|
|
|
if (preloader_.isPreloadedDataReady()) {
|
|
|
|
|
currentMedia_ = preloader_.swapAndRelease();
|
|
|
|
|
if (currentMedia_.isValid()) {
|
|
|
|
|
LOGI("감독: 새 배우 등장 준비 완료.");
|
|
|
|
|
isFading_ = false;
|
|
|
|
|
determineActiveAnimationMode();
|
|
|
|
|
animationCycleComplete_ = false; // 새로운 미디어는 애니메이션 사이클을 다시 시작
|
|
|
|
|
currentOffsetX_ = 0.0f; currentOffsetY_ = 0.0f; currentZoom_ = 1.0f;
|
|
|
|
|
xDirection_ = 1; yDirection_ = 1; zoomDirection_ = 1;
|
|
|
|
|
|
|
|
|
|
// PAGE_TURN 모드일 경우 대기 시간 시작
|
|
|
|
|
if (activeAnimationMode_ == AnimationMode::PAGE_TURN) {
|
|
|
|
|
pageTurnStartTime_ = now;
|
|
|
|
|
LOGI("PAGE_TURN mode: Starting delay for %lld ms.", pageTurnDelayMs_);
|
2025-08-27 15:09:05 +09:00
|
|
|
}
|
2025-08-28 15:13:40 +09:00
|
|
|
callNextMediaCallback(); // 다음 미디어 미리 로드 요청
|
2025-08-27 15:09:05 +09:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-28 15:13:40 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 현재 미디어가 없으면 검은 화면만 출력
|
|
|
|
|
if (!currentMedia_.isValid()) {
|
|
|
|
|
ANativeWindow_Buffer buffer;
|
|
|
|
|
if (ANativeWindow_lock(window, &buffer, nullptr) == 0) {
|
|
|
|
|
memset(buffer.bits, 0, buffer.stride * buffer.height * sizeof(uint32_t));
|
|
|
|
|
ANativeWindow_unlockAndPost(window);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
// 2. 다음 미디어 미리 로딩
|
|
|
|
|
if (!nextMedia_.isValid() && preloader_.isPreloadedDataReady()) {
|
|
|
|
|
nextMedia_ = preloader_.swapAndRelease();
|
|
|
|
|
if(nextMedia_.isValid()) {
|
|
|
|
|
LOGI("감독: 다음 배우 대기실에서 준비 완료.");
|
|
|
|
|
callNextMediaCallback();
|
2025-08-27 15:09:05 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
// 3. 애니메이션 상태 업데이트
|
|
|
|
|
// PAGE_TURN 모드의 대기 시간 처리
|
|
|
|
|
if (activeAnimationMode_ == AnimationMode::PAGE_TURN && !isFading_) {
|
|
|
|
|
long long elapsedDelay = std::chrono::duration_cast<std::chrono::milliseconds>(now - pageTurnStartTime_).count();
|
|
|
|
|
if (elapsedDelay >= pageTurnDelayMs_) {
|
|
|
|
|
animationCycleComplete_ = true; // 대기 시간 경과, 페이드 시작 준비
|
|
|
|
|
LOGI("PAGE_TURN mode: Delay complete, preparing for fade.");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
// 일반 애니메이션 (PAN, ZOOM 등) 업데이트
|
|
|
|
|
if (!isFading_ && !animationCycleComplete_) {
|
|
|
|
|
float overflowX = 0.0f, overflowY = 0.0f;
|
|
|
|
|
float mediaW = static_cast<float>(currentMedia_.getWidth());
|
|
|
|
|
float mediaH = static_cast<float>(currentMedia_.getHeight());
|
|
|
|
|
|
|
|
|
|
// 화면 비율에 맞춰 확대되었을 때, 넘치는 부분 계산
|
|
|
|
|
if ((mediaW / mediaH) > (static_cast<float>(surfaceWidth) / surfaceHeight)) {
|
|
|
|
|
float scale = static_cast<float>(surfaceHeight) / mediaH;
|
|
|
|
|
overflowX = std::max(0.0f, mediaW * scale - surfaceWidth);
|
2025-08-27 15:09:05 +09:00
|
|
|
} else {
|
2025-08-28 15:13:40 +09:00
|
|
|
float scale = static_cast<float>(surfaceWidth) / mediaW;
|
|
|
|
|
overflowY = std::max(0.0f, mediaH * scale - surfaceHeight);
|
2025-08-27 15:09:05 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
switch (activeAnimationMode_) {
|
|
|
|
|
case AnimationMode::PAN: {
|
|
|
|
|
bool xCycleCompleted = (overflowX <= 0);
|
|
|
|
|
bool yCycleCompleted = (overflowY <= 0);
|
|
|
|
|
if (overflowX > 0) {
|
|
|
|
|
currentOffsetX_ += animationSpeed_ * xDirection_;
|
|
|
|
|
if (xDirection_ == 1 && currentOffsetX_ >= overflowX) { currentOffsetX_ = overflowX; xDirection_ = -1; }
|
|
|
|
|
else if (xDirection_ == -1 && currentOffsetX_ <= 0) { currentOffsetX_ = 0; xDirection_ = 1; xCycleCompleted = true; }
|
|
|
|
|
}
|
|
|
|
|
if (overflowY > 0) {
|
|
|
|
|
currentOffsetY_ += animationSpeed_ * yDirection_;
|
|
|
|
|
if (yDirection_ == 1 && currentOffsetY_ >= overflowY) { currentOffsetY_ = overflowY; yDirection_ = -1; }
|
|
|
|
|
else if (yDirection_ == -1 && currentOffsetY_ <= 0) { currentOffsetY_ = 0; yDirection_ = 1; yCycleCompleted = true; }
|
|
|
|
|
}
|
|
|
|
|
if (xCycleCompleted && yCycleCompleted) { animationCycleComplete_ = true; }
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case AnimationMode::PAN_ONE_WAY: {
|
|
|
|
|
bool xReachedEnd = (overflowX <= 0);
|
|
|
|
|
bool yReachedEnd = (overflowY <= 0);
|
|
|
|
|
if (overflowX > 0) {
|
|
|
|
|
currentOffsetX_ += animationSpeed_;
|
|
|
|
|
if (currentOffsetX_ >= overflowX) { currentOffsetX_ = overflowX; xReachedEnd = true; }
|
|
|
|
|
}
|
|
|
|
|
if (overflowY > 0) {
|
|
|
|
|
currentOffsetY_ += animationSpeed_;
|
|
|
|
|
if (currentOffsetY_ >= overflowY) { currentOffsetY_ = overflowY; yReachedEnd = true; }
|
|
|
|
|
}
|
|
|
|
|
if (xReachedEnd && yReachedEnd) { animationCycleComplete_ = true; }
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case AnimationMode::ZOOM: {
|
|
|
|
|
currentZoom_ += 0.0005f * animationSpeed_ * zoomDirection_;
|
|
|
|
|
if (zoomDirection_ == 1 && currentZoom_ >= 1.2f) { currentZoom_ = 1.2f; zoomDirection_ = -1; }
|
|
|
|
|
else if (zoomDirection_ == -1 && currentZoom_ <= 1.0f) { currentZoom_ = 1.0f; zoomDirection_ = 1; animationCycleComplete_ = true; }
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case AnimationMode::NONE:
|
|
|
|
|
case AnimationMode::PAGE_TURN: // PAGE_TURN은 대기 시간 로직에서 animationCycleComplete_를 설정
|
|
|
|
|
default: {
|
|
|
|
|
animationCycleComplete_ = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. 페이드 전환 처리
|
|
|
|
|
if (animationCycleComplete_ && !isFading_ && nextMedia_.isValid()) {
|
2025-08-27 15:09:05 +09:00
|
|
|
isFading_ = true;
|
|
|
|
|
fadeStartTime_ = now;
|
2025-08-28 15:13:40 +09:00
|
|
|
LOGI("감독: 애니메이션 사이클 완료, 페이드 전환 시작.");
|
2025-08-27 15:09:05 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
float currentMediaAlpha = 1.0f;
|
|
|
|
|
float nextMediaAlpha = 0.0f;
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
if (isFading_) {
|
|
|
|
|
long long elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - fadeStartTime_).count();
|
|
|
|
|
currentMediaAlpha = std::clamp(1.0f - (float)elapsed / fadeDurationMs_, 0.0f, 1.0f);
|
|
|
|
|
nextMediaAlpha = std::clamp((float)elapsed / fadeDurationMs_, 0.0f, 1.0f);
|
|
|
|
|
|
|
|
|
|
if (elapsed >= fadeDurationMs_) { // 페이드 전환 완료 시
|
|
|
|
|
if (nextMedia_.isValid()) {
|
|
|
|
|
currentMedia_ = std::move(nextMedia_); // 다음 미디어를 현재 미디어로 교체
|
|
|
|
|
determineActiveAnimationMode(); // 새 미디어에 적용할 애니메이션 모드 결정
|
|
|
|
|
|
|
|
|
|
animationCycleComplete_ = false; // 새 미디어는 애니메이션 사이클 다시 시작
|
|
|
|
|
currentOffsetX_ = 0.0f; currentOffsetY_ = 0.0f; currentZoom_ = 1.0f;
|
|
|
|
|
xDirection_ = 1; yDirection_ = 1; zoomDirection_ = 1;
|
|
|
|
|
|
|
|
|
|
// PAGE_TURN 모드일 경우 대기 시간 다시 시작
|
|
|
|
|
if (activeAnimationMode_ == AnimationMode::PAGE_TURN) {
|
|
|
|
|
pageTurnStartTime_ = now;
|
|
|
|
|
LOGI("PAGE_TURN mode: Fade complete, starting new delay for %lld ms.", pageTurnDelayMs_);
|
|
|
|
|
} else {
|
|
|
|
|
LOGI("감독: 페이드 전환 완료, 새 배우 등장.");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
LOGW("감독: 페이드 완료되었으나 다음 배우가 준비되지 않음.");
|
|
|
|
|
}
|
|
|
|
|
isFading_ = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
// 5. 화면 그리기
|
|
|
|
|
ANativeWindow_Buffer buffer;
|
|
|
|
|
if (ANativeWindow_lock(window, &buffer, nullptr) != 0) return;
|
|
|
|
|
memset(buffer.bits, 0, buffer.stride * buffer.height * sizeof(uint32_t)); // 무대를 검은색으로 지움
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
// (A) 현재 배우 그리기
|
|
|
|
|
drawMedia(buffer, currentMedia_, currentMediaAlpha, currentOffsetX_, currentOffsetY_, currentZoom_);
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
// (B) 페이드 중이라면 다음 배우도 함께 그리기
|
|
|
|
|
if (nextMedia_.isValid() && isFading_) {
|
|
|
|
|
drawMedia(buffer, nextMedia_, nextMediaAlpha, 0.0f, 0.0f, 1.0f); // 다음 배우는 애니메이션 없이 중앙 정렬
|
2025-08-27 15:09:05 +09:00
|
|
|
}
|
|
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
ANativeWindow_unlockAndPost(window); // 화면 갱신
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float alpha, float offsetX, float offsetY, float scaleMultiplier) {
|
|
|
|
|
// 1. 기본 체크: 배우가 무대에 설 수 있는 상태인지 확인.
|
|
|
|
|
if (!media.isValid() || alpha <= 0.0f) return;
|
|
|
|
|
|
|
|
|
|
// 2. 기본 크기/위치 계산: 배우를 무대에 꽉 차게 중앙 정렬하기 위한 기본값 계산.
|
|
|
|
|
float baseScale, baseOffsetX, baseOffsetY;
|
|
|
|
|
calculateFitScaleAndOffset(media, buffer.width, buffer.height, baseScale, baseOffsetX, baseOffsetY);
|
|
|
|
|
|
|
|
|
|
// 3. 최종 크기/위치 계산: 감독의 지시(offsetX, offsetY, scaleMultiplier)를 기본값에 반영.
|
|
|
|
|
float finalScale = baseScale * scaleMultiplier;
|
|
|
|
|
float finalOffsetX = baseOffsetX + offsetX;
|
|
|
|
|
float finalOffsetY = baseOffsetY + offsetY;
|
2025-08-27 15:09:05 +09:00
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
// 4. 최종 지시 전달: 계산된 최종 값으로 실제 그림 그리는 담당자에게 작업을 넘김.
|
2025-08-27 15:09:05 +09:00
|
|
|
if (media.getType() == MediaAsset::Type::IMAGE) {
|
2025-08-28 15:13:40 +09:00
|
|
|
renderImageFrame(media, buffer, finalScale, finalOffsetX, finalOffsetY, alpha);
|
2025-08-27 15:09:05 +09:00
|
|
|
} else {
|
2025-08-28 15:13:40 +09:00
|
|
|
renderVideoFrame(media, buffer, finalScale, finalOffsetX, finalOffsetY, alpha);
|
2025-08-27 15:09:05 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Renderer::renderImageFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha) {
|
|
|
|
|
uint32_t* dstPixels = (uint32_t*)buffer.bits;
|
|
|
|
|
int dstStride = buffer.stride;
|
2025-08-28 15:13:40 +09:00
|
|
|
const uint8_t* pixelData = media.getType() == MediaAsset::Type::IMAGE ? media.getImageData() : media.getRgbBuffer().data();
|
|
|
|
|
if (!pixelData) return;
|
|
|
|
|
|
2025-08-27 15:09:05 +09:00
|
|
|
int imgW = media.getWidth();
|
|
|
|
|
int imgH = media.getHeight();
|
|
|
|
|
uint8_t alphaByte = static_cast<uint8_t>(alpha * 255.0f);
|
|
|
|
|
|
|
|
|
|
for (int y = 0; y < buffer.height; ++y) {
|
2025-08-28 15:13:40 +09:00
|
|
|
int srcY = static_cast<int>((y - offsetY) / scale);
|
2025-08-27 15:09:05 +09:00
|
|
|
if (srcY < 0 || srcY >= imgH) continue;
|
|
|
|
|
uint32_t* dstRow = dstPixels + y * dstStride;
|
|
|
|
|
for (int x = 0; x < buffer.width; ++x) {
|
2025-08-28 15:13:40 +09:00
|
|
|
int srcX = static_cast<int>((x - offsetX) / scale);
|
2025-08-27 15:09:05 +09:00
|
|
|
if (srcX < 0 || srcX >= imgW) continue;
|
|
|
|
|
const uint8_t* srcPixel = &pixelData[(srcY * imgW + srcX) * 4];
|
|
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
// Dst 픽셀이 BGRA일 수 있으므로, RGB로 변환하여 알파 블렌딩
|
|
|
|
|
uint32_t dstPixelValue = dstRow[x];
|
|
|
|
|
uint8_t dstB = (dstPixelValue >> 0) & 0xFF; // B component
|
|
|
|
|
uint8_t dstG = (dstPixelValue >> 8) & 0xFF; // G component
|
|
|
|
|
uint8_t dstR = (dstPixelValue >> 16) & 0xFF; // R component
|
2025-08-27 15:09:05 +09:00
|
|
|
|
|
|
|
|
uint8_t finalR = (srcPixel[0] * alphaByte + dstR * (255 - alphaByte)) / 255;
|
|
|
|
|
uint8_t finalG = (srcPixel[1] * alphaByte + dstG * (255 - alphaByte)) / 255;
|
|
|
|
|
uint8_t finalB = (srcPixel[2] * alphaByte + dstB * (255 - alphaByte)) / 255;
|
|
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
// 출력 버퍼는 보통 BGRA (Android ARGB_8888은 BGRA 순서로 메모리에 저장됨)
|
2025-08-27 15:09:05 +09:00
|
|
|
dstRow[x] = (0xFF << 24) | (finalR << 16) | (finalG << 8) | finalB;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
void Renderer::renderVideoFrame(MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha) {
|
2025-08-27 15:09:05 +09:00
|
|
|
AVFormatContext* fmt_ctx = media.getFormatContext();
|
|
|
|
|
AVCodecContext* codec_ctx = media.getCodecContext();
|
|
|
|
|
AVFrame* frame = media.getFrame();
|
|
|
|
|
AVPacket* pkt = media.getPacket();
|
|
|
|
|
SwsContext* sws_ctx = media.getSwsContext();
|
|
|
|
|
int video_stream_idx = media.getVideoStreamIndex();
|
|
|
|
|
|
|
|
|
|
if (!fmt_ctx || !codec_ctx || !frame || !pkt || !sws_ctx) return;
|
|
|
|
|
|
|
|
|
|
int ret = av_read_frame(fmt_ctx, pkt);
|
|
|
|
|
if (ret >= 0) {
|
|
|
|
|
if (pkt->stream_index == video_stream_idx) {
|
|
|
|
|
if (avcodec_send_packet(codec_ctx, pkt) >= 0) {
|
|
|
|
|
if (avcodec_receive_frame(codec_ctx, frame) == 0) {
|
2025-08-28 15:13:40 +09:00
|
|
|
std::vector<uint8_t>& rgbBuf = media.getRgbBuffer();
|
2025-08-27 15:09:05 +09:00
|
|
|
uint8_t* dst[4] = { rgbBuf.data(), nullptr, nullptr, nullptr };
|
|
|
|
|
int dstStride_arr[4] = { media.getWidth() * 4, 0, 0, 0 };
|
|
|
|
|
sws_scale(sws_ctx, frame->data, frame->linesize, 0, media.getHeight(), dst, dstStride_arr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
av_packet_unref(pkt);
|
2025-08-28 15:13:40 +09:00
|
|
|
} else if (ret == AVERROR_EOF) { // 비디오 끝에 도달하면 처음으로 되감기
|
2025-08-27 15:09:05 +09:00
|
|
|
av_seek_frame(fmt_ctx, video_stream_idx, 0, AVSEEK_FLAG_BACKWARD);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-28 15:13:40 +09:00
|
|
|
// 디코딩된 비디오 프레임은 이미지 데이터처럼 RGB 버퍼에 저장되어 있으므로 renderImageFrame을 사용
|
2025-08-27 15:09:05 +09:00
|
|
|
renderImageFrame(media, buffer, scale, offsetX, offsetY, alpha);
|
|
|
|
|
}
|