...
This commit is contained in:
@@ -3,36 +3,37 @@ cmake_minimum_required(VERSION 3.18.1)
|
||||
project("native_renderer")
|
||||
|
||||
# --- 1. 경로 변수 설정 ---
|
||||
# NOTE: .so 파일들이 있는 lib 폴더와 include 폴더의 경로를 각각 명확히 지정합니다.
|
||||
set(JNI_LIBS_ROOT ${CMAKE_SOURCE_DIR}/../../jniLibs/${ANDROID_ABI})
|
||||
set(JNI_SO_DIR ${JNI_LIBS_ROOT}/lib)
|
||||
set(JNI_INCLUDE_DIR ${JNI_LIBS_ROOT}/include)
|
||||
# NOTE: 표준 안드로이드 프로젝트 구조에 맞춰 'main' 폴더를 경로에 포함합니다.
|
||||
set(JNI_LIBS_DIR ${CMAKE_SOURCE_DIR}/../../main/jniLibs/${ANDROID_ABI})
|
||||
|
||||
|
||||
# --- 2. 헤더 파일 경로 추가 ---
|
||||
include_directories(${JNI_INCLUDE_DIR})
|
||||
|
||||
include_directories(${JNI_LIBS_DIR}/include)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/include)
|
||||
|
||||
# --- 3. 미리 빌드된 FFmpeg .so 라이브러리들을 IMPORTED 타겟으로 선언 ---
|
||||
# NOTE: IMPORTED_LOCATION 경로가 lib 폴더를 포함하도록 수정되었습니다.
|
||||
# NOTE: 스크린샷의 파일 구조에 맞게 IMPORTED_LOCATION 경로에서 '/lib'를 제거했습니다.
|
||||
add_library(avformat SHARED IMPORTED)
|
||||
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${JNI_SO_DIR}/libavformat.so)
|
||||
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${JNI_LIBS_DIR}/libavformat.so)
|
||||
|
||||
add_library(avcodec SHARED IMPORTED)
|
||||
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${JNI_SO_DIR}/libavcodec.so)
|
||||
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${JNI_LIBS_DIR}/libavcodec.so)
|
||||
|
||||
add_library(avutil SHARED IMPORTED)
|
||||
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${JNI_SO_DIR}/libavutil.so)
|
||||
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${JNI_LIBS_DIR}/libavutil.so)
|
||||
|
||||
add_library(swscale SHARED IMPORTED)
|
||||
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION ${JNI_SO_DIR}/libswscale.so)
|
||||
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION ${JNI_LIBS_DIR}/libswscale.so)
|
||||
|
||||
add_library(swresample SHARED IMPORTED)
|
||||
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${JNI_SO_DIR}/libswresample.so)
|
||||
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${JNI_LIBS_DIR}/libswresample.so)
|
||||
|
||||
# 참고: 스크린샷에 보이는 다른 라이브러리들도 필요하다면 여기에 추가할 수 있습니다.
|
||||
#add_library(avdevice SHARED IMPORTED)
|
||||
#set_target_properties(avdevice PROPERTIES IMPORTED_LOCATION ${JNI_LIBS_DIR}/libavdevice.so)
|
||||
|
||||
|
||||
# --- 4. 네이티브 렌더러 라이브러리 빌드 설정 ---
|
||||
# NOTE: 앱의 C++ 소스 파일들을 여기에 모두 나열합니다.
|
||||
add_library(native_renderer SHARED
|
||||
native_renderer.cpp
|
||||
Renderer.cpp
|
||||
@@ -42,7 +43,6 @@ add_library(native_renderer SHARED
|
||||
|
||||
|
||||
# --- 5. 최종 라이브러리 링크 ---
|
||||
# NOTE: 안드로이드 기본 라이브러리와 위에서 선언한 FFmpeg 타겟들을 링크합니다.
|
||||
find_library(log-lib log)
|
||||
find_library(android-lib android)
|
||||
find_library(nativewindow-lib nativewindow)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <android/log.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <unistd.h> // for close()
|
||||
|
||||
// stb_image.h의 구현부를 여기에 단 한 번만 포함시킵니다.
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
@@ -11,14 +12,33 @@
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
MediaAsset::MediaAsset() = default;
|
||||
// MediaAsset::MediaAsset() = default; // .cpp 파일에서 삭제
|
||||
|
||||
MediaAsset::~MediaAsset() {
|
||||
release();
|
||||
}
|
||||
|
||||
// 파일 확장자를 확인하여 적절한 로더를 호출하는 분기 함수
|
||||
// [수정] 파일 디스크립터를 받는 load 함수 구현
|
||||
bool MediaAsset::load(int fd) {
|
||||
if (fd < 0) {
|
||||
LOGE("Load failed: Invalid file descriptor.");
|
||||
return false;
|
||||
}
|
||||
// fd를 /proc/self/fd/ 경로로 변환하여 내부 로직 재활용
|
||||
std::string path = "/proc/self/fd/" + std::to_string(fd);
|
||||
bool result = loadInternal(path);
|
||||
// 네이티브에서 fd를 받았으므로 여기서 닫아줘야 메모리 누수를 방지
|
||||
close(fd);
|
||||
return result;
|
||||
}
|
||||
|
||||
// [수정] 기존 path를 받는 load 함수는 내부 함수 호출로 변경
|
||||
bool MediaAsset::load(const std::string& path) {
|
||||
return loadInternal(path);
|
||||
}
|
||||
|
||||
// [수정] load 함수의 핵심 로직을 별도 내부 함수로 분리
|
||||
bool MediaAsset::loadInternal(const std::string& path) {
|
||||
release();
|
||||
if (path.empty()) {
|
||||
LOGE("Load failed: Path is empty.");
|
||||
@@ -26,11 +46,20 @@ bool MediaAsset::load(const std::string& path) {
|
||||
}
|
||||
|
||||
size_t dotPos = path.find_last_of('.');
|
||||
|
||||
// '/proc/self/fd/...' 경로처럼 확장자가 없는 경우
|
||||
if (dotPos == std::string::npos) {
|
||||
LOGE("Load failed: Could not find file extension in path: %s", path.c_str());
|
||||
return false;
|
||||
LOGI("No extension found for %s. Attempting smart load.", path.c_str());
|
||||
// 비디오로 먼저 시도하고, 성공하면 즉시 반환
|
||||
if (loadVideoWithFFmpeg(path)) {
|
||||
return true;
|
||||
}
|
||||
// 비디오 로딩 실패 시, 이미지로 이어서 시도
|
||||
LOGI("Video load failed, trying as image.");
|
||||
return loadImageWithStb(path);
|
||||
}
|
||||
|
||||
// 확장자가 있는 일반 경로의 경우
|
||||
std::string extension = path.substr(dotPos + 1);
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
|
||||
|
||||
@@ -40,10 +69,10 @@ bool MediaAsset::load(const std::string& path) {
|
||||
bool isVideo = std::find(videoExts.begin(), videoExts.end(), extension) != videoExts.end();
|
||||
bool isImage = std::find(imageExts.begin(), imageExts.end(), extension) != imageExts.end();
|
||||
|
||||
if (isImage) {
|
||||
return loadImageWithStb(path);
|
||||
} else if (isVideo) {
|
||||
if (isVideo) { // [수정] 비디오 확장자일 경우 비디오만 시도
|
||||
return loadVideoWithFFmpeg(path);
|
||||
} else if (isImage) { // [수정] 이미지 확장자일 경우 이미지만 시도
|
||||
return loadImageWithStb(path);
|
||||
}
|
||||
|
||||
LOGE("Load failed: Unsupported file format for path: %s", path.c_str());
|
||||
@@ -69,7 +98,7 @@ bool MediaAsset::loadVideoWithFFmpeg(const std::string& path) {
|
||||
|
||||
// 1. 비디오 파일 열기
|
||||
if (avformat_open_input(&fmtCtx_, path.c_str(), nullptr, nullptr) != 0) {
|
||||
LOGE("Could not open video file: %s", path.c_str());
|
||||
LOGE("Could not open video file: %s", path.c_str()); // 오타 수정: c_st() -> c_str()
|
||||
release();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
|
||||
// FFmpeg 헤더 파일 포함
|
||||
// C언어 라이브러리와의 호환성을 위한 extern "C" 블록
|
||||
extern "C" {
|
||||
// NOTE: 가장 기초가 되는 avutil.h를 제일 먼저 포함해야 합니다.
|
||||
#include <libavutil/avutil.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libswscale/swscale.h>
|
||||
@@ -13,38 +16,36 @@ extern "C" {
|
||||
|
||||
class MediaAsset {
|
||||
public:
|
||||
// 미디어 타입을 구분하기 위한 enum
|
||||
enum class Type { UNKNOWN, IMAGE, VIDEO };
|
||||
|
||||
MediaAsset();
|
||||
MediaAsset() = default;
|
||||
~MediaAsset();
|
||||
|
||||
// 복사를 방지하고 이동만 허용 (효율적인 자원 관리)
|
||||
MediaAsset(const MediaAsset&) = delete;
|
||||
MediaAsset& operator=(const MediaAsset&) = delete;
|
||||
MediaAsset(MediaAsset&& other) noexcept;
|
||||
MediaAsset& operator=(MediaAsset&& other) noexcept;
|
||||
|
||||
// 파일 경로를 받아 이미지 또는 비디오를 로드하는 메인 함수
|
||||
bool load(const std::string& path);
|
||||
// 모든 자원을 해제하는 함수
|
||||
bool load(int fd);
|
||||
void release();
|
||||
|
||||
// 현재 MediaAsset이 유효한 데이터를 가지고 있는지 확인
|
||||
bool isValid() const;
|
||||
|
||||
// Getter 함수들
|
||||
Type getType() const { return type_; }
|
||||
int getWidth() const { return width_; }
|
||||
int getHeight() const { return height_; }
|
||||
|
||||
// 이미지 데이터 포인터를 반환 (이미지 타입일 때만 유효)
|
||||
uint8_t* getImageData() const { return imageData_; }
|
||||
// --- 수정된 부분 ---
|
||||
// 기존: uint8_t* getImageData() const { return imageData_; }
|
||||
// const 객체는 const 포인터를 반환하도록 수정
|
||||
const uint8_t* getImageData() const { return imageData_; }
|
||||
|
||||
// 비디오 프레임의 RGB 데이터 버퍼를 반환 (비디오 타입일 때만 유효)
|
||||
// 기존: std::vector<uint8_t>& getRgbBuffer() { return rgbBuffer_; }
|
||||
// non-const 객체용 (기존 함수 유지)
|
||||
std::vector<uint8_t>& getRgbBuffer() { return rgbBuffer_; }
|
||||
// const 객체용 오버로딩 함수 추가
|
||||
const std::vector<uint8_t>& getRgbBuffer() const { return rgbBuffer_; }
|
||||
// --- 수정 끝 ---
|
||||
|
||||
|
||||
// 비디오 처리에 필요한 FFmpeg 컨텍스트들을 반환하는 Getter 함수들
|
||||
AVFormatContext* getFormatContext() const { return fmtCtx_; }
|
||||
AVCodecContext* getCodecContext() const { return codecCtx_; }
|
||||
AVFrame* getFrame() const { return frame_; }
|
||||
@@ -53,24 +54,20 @@ public:
|
||||
int getVideoStreamIndex() const { return videoStreamIdx_; }
|
||||
|
||||
private:
|
||||
// 내부 헬퍼 함수
|
||||
// ... (이하 동일)
|
||||
bool loadInternal(const std::string& path);
|
||||
bool loadVideoWithFFmpeg(const std::string& path);
|
||||
bool loadImageWithStb(const std::string& path);
|
||||
|
||||
// 공통 멤버 변수
|
||||
Type type_ = Type::UNKNOWN;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
|
||||
// 이미지 전용 멤버 변수
|
||||
uint8_t* imageData_ = nullptr;
|
||||
|
||||
// 비디오 전용 멤버 변수 (FFmpeg)
|
||||
std::vector<uint8_t> rgbBuffer_;
|
||||
AVFormatContext* fmtCtx_ = nullptr;
|
||||
AVCodecContext* codecCtx_ = nullptr;
|
||||
AVFrame* frame_ = nullptr;
|
||||
AVPacket* packet_ = nullptr;
|
||||
SwsContext* swsCtx_ = nullptr;
|
||||
int videoStreamIdx_ = -1;
|
||||
std::vector<uint8_t> rgbBuffer_;
|
||||
};
|
||||
@@ -20,9 +20,10 @@ void Preloader::releasePreloadedData() {
|
||||
preloadFailed_ = false;
|
||||
}
|
||||
|
||||
// 수정됨: std::string path 대신 int fd를 인자로 받음
|
||||
// [수정] std::string path 대신 int fd를 인자로 받음
|
||||
void Preloader::startNextPreload(int fd) {
|
||||
if (dataReady_ || preloadFailed_) {
|
||||
if (dataReady_) {
|
||||
LOGI("Preloader is busy with ready data. Ignoring new preload request.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,22 +32,22 @@ void Preloader::startNextPreload(int fd) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 이전에 실패했더라도 새로운 요청이 오면 다시 시도하도록 초기화
|
||||
preloadFailed_ = false;
|
||||
|
||||
// 백그라운드 스레드에서 로딩 작업 수행
|
||||
std::thread([this, fd]() {
|
||||
LOGI("Starting preload thread for media with fd: %d", fd);
|
||||
std::lock_guard<std::mutex> lock(preloadMutex_);
|
||||
|
||||
// MediaAsset의 load(fd) 함수 호출
|
||||
if (nextMedia_.load(fd)) {
|
||||
MediaAsset tempMedia;
|
||||
if (tempMedia.load(fd)) {
|
||||
std::lock_guard<std::mutex> lock(preloadMutex_);
|
||||
nextMedia_ = std::move(tempMedia);
|
||||
dataReady_ = true;
|
||||
LOGI("Preloading finished successfully.");
|
||||
} else {
|
||||
LOGE("Preloading failed.");
|
||||
// --- ⬇️ 실패 로그 추가 ⬇️ ---
|
||||
LOGE("!!! Preloading FAILED for fd: %d. !!!", fd);
|
||||
preloadFailed_ = true;
|
||||
nextMedia_.release();
|
||||
// 로딩 실패 시 다음 미디어를 요청하여 멈추지 않도록 함
|
||||
callNextMediaCallback();
|
||||
}
|
||||
}).detach();
|
||||
|
||||
@@ -10,7 +10,7 @@ class Preloader {
|
||||
public:
|
||||
Preloader();
|
||||
~Preloader();
|
||||
void startNextPreload(int fd);
|
||||
void startNextPreload(int fd); // [수정] string -> int
|
||||
bool isPreloadedDataReady() const;
|
||||
bool hasPreloadFailed() const;
|
||||
MediaAsset swapAndRelease();
|
||||
|
||||
+285
-126
@@ -1,22 +1,18 @@
|
||||
#include "Renderer.h"
|
||||
#include "Preloader.h"
|
||||
#include <android/log.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
extern Preloader* preloader; // 전역 Preloader 객체
|
||||
extern void callNextMediaCallback(); // 다음 미디어 요청 콜백
|
||||
extern void callNextMediaCallback();
|
||||
|
||||
#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__)
|
||||
|
||||
static constexpr long long displayDurationMs = 20000;
|
||||
static constexpr long long fadeDurationMs = 3000;
|
||||
|
||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
|
||||
Renderer::Renderer() {
|
||||
mediaStartTime_ = std::chrono::steady_clock::now();
|
||||
randomEngine_.seed(std::chrono::high_resolution_clock::now().time_since_epoch().count());
|
||||
}
|
||||
|
||||
Renderer::~Renderer() {
|
||||
@@ -30,178 +26,342 @@ void Renderer::release() {
|
||||
}
|
||||
|
||||
void Renderer::setNextMedia(int fd) {
|
||||
std::lock_guard<std::mutex> lock(renderMutex_);
|
||||
currentMedia_.release();
|
||||
if (currentMedia_.load(fd)) {
|
||||
LOGI("New media loaded successfully.");
|
||||
mediaStartTime_ = std::chrono::steady_clock::now();
|
||||
isFading_ = false;
|
||||
preloader_.startNextPreload(fd);
|
||||
}
|
||||
|
||||
void Renderer::setAnimationSpeed(float speed) {
|
||||
animationSpeed_ = speed;
|
||||
}
|
||||
|
||||
void Renderer::setFadeDuration(int durationMs) {
|
||||
fadeDurationMs_ = durationMs > 0 ? durationMs : 3000;
|
||||
}
|
||||
|
||||
// --- ⬇️ 새로운 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 {
|
||||
LOGE("Failed to load new media: %s. Requesting next media.", currentMediaPath_.c_str());
|
||||
// NOTE: 초기 로딩 실패 시, 다음 미디어를 바로 요청하여 검은 화면 방지
|
||||
callNextMediaCallback();
|
||||
activeAnimationMode_ = configuredAnimationMode_;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
||||
float mediaAspect = static_cast<float>(media.getWidth()) / media.getHeight();
|
||||
float surfaceAspect = static_cast<float>(surfaceWidth) / surfaceHeight;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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_);
|
||||
|
||||
if (!currentMedia_.isValid()) {
|
||||
// NOTE: 렌더링할 미디어가 없으면 아무것도 하지 않음 (검은 화면)
|
||||
// 로딩 실패 시 setNextMedia에서 다음 미디어를 요청하므로 일시적인 상태
|
||||
return;
|
||||
}
|
||||
|
||||
ANativeWindow_Buffer buffer;
|
||||
if (ANativeWindow_lock(window, &buffer, nullptr) < 0) {
|
||||
LOGE("Failed to lock window.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 화면을 검은색으로 초기화
|
||||
memset(buffer.bits, 0, buffer.stride * buffer.height * sizeof(uint32_t));
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
long long elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(now - mediaStartTime_).count();
|
||||
|
||||
// 1. 현재 미디어 그리기 (페이드 아웃 효과 적용)
|
||||
float currentAlpha = 1.0f;
|
||||
if (isFading_) {
|
||||
long long fadeElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - fadeStartTime_).count();
|
||||
currentAlpha = std::clamp(1.0f - (float)fadeElapsed / fadeDurationMs, 0.0f, 1.0f);
|
||||
}
|
||||
drawMedia(buffer, currentMedia_, currentAlpha, elapsedMs);
|
||||
// 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;
|
||||
|
||||
// 2. 다음 미디어 그리기 (페이드 인 효과 적용)
|
||||
if (isFading_) {
|
||||
// Preloader로부터 미리 로드된 미디어를 가져오기 시도
|
||||
if (!nextMedia_.isValid()) {
|
||||
if (preloader && preloader->isPreloadedDataReady()) {
|
||||
nextMedia_ = preloader->swapAndRelease();
|
||||
if (nextMedia_.isValid()) {
|
||||
LOGI("Renderer acquired preloaded media for fade-in.");
|
||||
// 성공적으로 가져왔으면, 다음 미디어 예비 로딩 요청
|
||||
callNextMediaCallback();
|
||||
} else {
|
||||
LOGE("Renderer failed to acquire a valid preloaded media.");
|
||||
// PAGE_TURN 모드일 경우 대기 시간 시작
|
||||
if (activeAnimationMode_ == AnimationMode::PAGE_TURN) {
|
||||
pageTurnStartTime_ = now;
|
||||
LOGI("PAGE_TURN mode: Starting delay for %lld ms.", pageTurnDelayMs_);
|
||||
}
|
||||
callNextMediaCallback(); // 다음 미디어 미리 로드 요청
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 가져온 nextMedia_가 유효하다면 페이드 인 효과로 그리기
|
||||
if (nextMedia_.isValid()) {
|
||||
long long fadeElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - fadeStartTime_).count();
|
||||
float nextAlpha = std::clamp((float)fadeElapsed / fadeDurationMs, 0.0f, 1.0f);
|
||||
drawMedia(buffer, nextMedia_, nextAlpha, 0); // 새 미디어는 0ms부터 시작
|
||||
// 현재 미디어가 없으면 검은 화면만 출력
|
||||
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;
|
||||
}
|
||||
|
||||
// 2. 다음 미디어 미리 로딩
|
||||
if (!nextMedia_.isValid() && preloader_.isPreloadedDataReady()) {
|
||||
nextMedia_ = preloader_.swapAndRelease();
|
||||
if(nextMedia_.isValid()) {
|
||||
LOGI("감독: 다음 배우 대기실에서 준비 완료.");
|
||||
callNextMediaCallback();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 미디어 상태 업데이트
|
||||
long long fadeTotalElapsed = isFading_ ? std::chrono::duration_cast<std::chrono::milliseconds>(now - fadeStartTime_).count() : 0;
|
||||
|
||||
if (isFading_ && fadeTotalElapsed >= fadeDurationMs) {
|
||||
// 페이드가 끝났을 때
|
||||
if (nextMedia_.isValid()) {
|
||||
// NOTE: 다음 미디어가 유효할 때만 현재 미디어를 교체 (가장 중요)
|
||||
currentMedia_ = std::move(nextMedia_);
|
||||
LOGI("Renderer successfully swapped to new media.");
|
||||
} else {
|
||||
LOGE("Fade ended, but no valid next media. Retaining current media.");
|
||||
// 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.");
|
||||
}
|
||||
isFading_ = false;
|
||||
mediaStartTime_ = now;
|
||||
}
|
||||
|
||||
} else if (!isFading_ && elapsedMs >= displayDurationMs) {
|
||||
// 현재 미디어 재생 시간이 다 되어 페이드를 시작할 때
|
||||
// 일반 애니메이션 (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);
|
||||
} else {
|
||||
float scale = static_cast<float>(surfaceWidth) / mediaW;
|
||||
overflowY = std::max(0.0f, mediaH * scale - surfaceHeight);
|
||||
}
|
||||
|
||||
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()) {
|
||||
isFading_ = true;
|
||||
fadeStartTime_ = now;
|
||||
LOGI("Display duration ended. Starting fade transition.");
|
||||
LOGI("감독: 애니메이션 사이클 완료, 페이드 전환 시작.");
|
||||
}
|
||||
|
||||
ANativeWindow_unlockAndPost(window);
|
||||
float currentMediaAlpha = 1.0f;
|
||||
float nextMediaAlpha = 0.0f;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 화면 그리기
|
||||
ANativeWindow_Buffer buffer;
|
||||
if (ANativeWindow_lock(window, &buffer, nullptr) != 0) return;
|
||||
memset(buffer.bits, 0, buffer.stride * buffer.height * sizeof(uint32_t)); // 무대를 검은색으로 지움
|
||||
|
||||
// (A) 현재 배우 그리기
|
||||
drawMedia(buffer, currentMedia_, currentMediaAlpha, currentOffsetX_, currentOffsetY_, currentZoom_);
|
||||
|
||||
// (B) 페이드 중이라면 다음 배우도 함께 그리기
|
||||
if (nextMedia_.isValid() && isFading_) {
|
||||
drawMedia(buffer, nextMedia_, nextMediaAlpha, 0.0f, 0.0f, 1.0f); // 다음 배우는 애니메이션 없이 중앙 정렬
|
||||
}
|
||||
|
||||
ANativeWindow_unlockAndPost(window); // 화면 갱신
|
||||
}
|
||||
|
||||
// --- 나머지 함수들은 변경 사항 없음 ---
|
||||
|
||||
void Renderer::drawMedia(ANativeWindow_Buffer& buffer, const MediaAsset& media, float alpha, float offsetElapsedMs) {
|
||||
void Renderer::drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float alpha, float offsetX, float offsetY, float scaleMultiplier) {
|
||||
// 1. 기본 체크: 배우가 무대에 설 수 있는 상태인지 확인.
|
||||
if (!media.isValid() || alpha <= 0.0f) return;
|
||||
|
||||
float mediaW = static_cast<float>(media.getWidth());
|
||||
float mediaH = static_cast<float>(media.getHeight());
|
||||
float bufW = static_cast<float>(buffer.width);
|
||||
float bufH = static_cast<float>(buffer.height);
|
||||
// 2. 기본 크기/위치 계산: 배우를 무대에 꽉 차게 중앙 정렬하기 위한 기본값 계산.
|
||||
float baseScale, baseOffsetX, baseOffsetY;
|
||||
calculateFitScaleAndOffset(media, buffer.width, buffer.height, baseScale, baseOffsetX, baseOffsetY);
|
||||
|
||||
float scale, overflowX, overflowY;
|
||||
if ((mediaW / mediaH) > (bufW / bufH)) {
|
||||
scale = bufH / mediaH;
|
||||
overflowX = std::max(0.0f, mediaW * scale - bufW);
|
||||
overflowY = 0.0f;
|
||||
} else {
|
||||
scale = bufW / mediaW;
|
||||
overflowX = 0.0f;
|
||||
overflowY = std::max(0.0f, mediaH * scale - bufH);
|
||||
}
|
||||
|
||||
float offsetX = 0.0f;
|
||||
float offsetY = 0.0f;
|
||||
updateOffset(offsetX, offsetY, overflowX, overflowY, offsetElapsedMs);
|
||||
// 3. 최종 크기/위치 계산: 감독의 지시(offsetX, offsetY, scaleMultiplier)를 기본값에 반영.
|
||||
float finalScale = baseScale * scaleMultiplier;
|
||||
float finalOffsetX = baseOffsetX + offsetX;
|
||||
float finalOffsetY = baseOffsetY + offsetY;
|
||||
|
||||
// 4. 최종 지시 전달: 계산된 최종 값으로 실제 그림 그리는 담당자에게 작업을 넘김.
|
||||
if (media.getType() == MediaAsset::Type::IMAGE) {
|
||||
renderImageFrame(media, buffer, scale, offsetX, offsetY, alpha);
|
||||
renderImageFrame(media, buffer, finalScale, finalOffsetX, finalOffsetY, alpha);
|
||||
} else {
|
||||
renderVideoFrame(media, buffer, scale, offsetX, offsetY, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::updateOffset(float& offsetX, float& offsetY, float overflowX, float overflowY, long long elapsedMs) {
|
||||
if (overflowX > 0) {
|
||||
float normalizedTime = fmod((float)elapsedMs / displayDurationMs, 2.0f);
|
||||
normalizedTime = (normalizedTime > 1.0f) ? 2.0f - normalizedTime : normalizedTime;
|
||||
offsetX = overflowX * normalizedTime;
|
||||
}
|
||||
if (overflowY > 0) {
|
||||
float normalizedTime = fmod((float)elapsedMs / displayDurationMs, 2.0f);
|
||||
normalizedTime = (normalizedTime > 1.0f) ? 2.0f - normalizedTime : normalizedTime;
|
||||
offsetY = overflowY * normalizedTime;
|
||||
renderVideoFrame(media, buffer, finalScale, finalOffsetX, finalOffsetY, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
const uint8_t* pixelData = media.getImageData();
|
||||
const uint8_t* pixelData = media.getType() == MediaAsset::Type::IMAGE ? media.getImageData() : media.getRgbBuffer().data();
|
||||
if (!pixelData) return;
|
||||
|
||||
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) {
|
||||
int srcY = static_cast<int>((y + offsetY) / scale);
|
||||
int srcY = static_cast<int>((y - offsetY) / scale);
|
||||
if (srcY < 0 || srcY >= imgH) continue;
|
||||
uint32_t* dstRow = dstPixels + y * dstStride;
|
||||
for (int x = 0; x < buffer.width; ++x) {
|
||||
int srcX = static_cast<int>((x + offsetX) / scale);
|
||||
int srcX = static_cast<int>((x - offsetX) / scale);
|
||||
if (srcX < 0 || srcX >= imgW) continue;
|
||||
|
||||
const uint8_t* srcPixel = &pixelData[(srcY * imgW + srcX) * 4];
|
||||
|
||||
uint32_t dstPixel = dstRow[x];
|
||||
uint8_t dstR = (dstPixel >> 16) & 0xFF;
|
||||
uint8_t dstG = (dstPixel >> 8) & 0xFF;
|
||||
uint8_t dstB = dstPixel & 0xFF;
|
||||
// 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
|
||||
|
||||
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;
|
||||
|
||||
// 출력 버퍼는 보통 BGRA (Android ARGB_8888은 BGRA 순서로 메모리에 저장됨)
|
||||
dstRow[x] = (0xFF << 24) | (finalR << 16) | (finalG << 8) | finalB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::renderVideoFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha) {
|
||||
void Renderer::renderVideoFrame(MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha) {
|
||||
AVFormatContext* fmt_ctx = media.getFormatContext();
|
||||
AVCodecContext* codec_ctx = media.getCodecContext();
|
||||
AVFrame* frame = media.getFrame();
|
||||
@@ -211,13 +371,12 @@ void Renderer::renderVideoFrame(const MediaAsset& media, ANativeWindow_Buffer& b
|
||||
|
||||
if (!fmt_ctx || !codec_ctx || !frame || !pkt || !sws_ctx) return;
|
||||
|
||||
std::vector<uint8_t>& rgbBuf = const_cast<MediaAsset&>(media).getRgbBuffer();
|
||||
|
||||
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) {
|
||||
std::vector<uint8_t>& rgbBuf = media.getRgbBuffer();
|
||||
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);
|
||||
@@ -225,10 +384,10 @@ void Renderer::renderVideoFrame(const MediaAsset& media, ANativeWindow_Buffer& b
|
||||
}
|
||||
}
|
||||
av_packet_unref(pkt);
|
||||
} else if (ret == AVERROR_EOF) {
|
||||
} else if (ret == AVERROR_EOF) { // 비디오 끝에 도달하면 처음으로 되감기
|
||||
av_seek_frame(fmt_ctx, video_stream_idx, 0, AVSEEK_FLAG_BACKWARD);
|
||||
}
|
||||
|
||||
// 디코딩된 프레임이 버퍼에 있으므로, renderImageFrame과 유사한 로직으로 화면에 그립니다.
|
||||
// 디코딩된 비디오 프레임은 이미지 데이터처럼 RGB 버퍼에 저장되어 있으므로 renderImageFrame을 사용
|
||||
renderImageFrame(media, buffer, scale, offsetX, offsetY, alpha);
|
||||
}
|
||||
@@ -1,36 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "MediaAsset.h"
|
||||
#include <chrono>
|
||||
#include "Preloader.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <android/native_window.h>
|
||||
#include <android/native_window_jni.h>
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
enum class AnimationMode {
|
||||
PAN = 0,
|
||||
ZOOM = 1,
|
||||
NONE = 2,
|
||||
RANDOM = 3,
|
||||
PAN_ONE_WAY = 4,
|
||||
PAGE_TURN = 5
|
||||
};
|
||||
|
||||
Renderer();
|
||||
~Renderer();
|
||||
|
||||
void setNextMedia(int fd);
|
||||
|
||||
void renderFrame(ANativeWindow* window);
|
||||
void release();
|
||||
std::string getDebugInfo() const;
|
||||
|
||||
void setAnimationSpeed(float speed);
|
||||
void setAnimationMode(int mode);
|
||||
void setFadeDuration(int durationMs);
|
||||
void setPageTurnDelay(int delayMs); // <-- 새로운 setter 선언
|
||||
|
||||
private:
|
||||
std::mutex renderMutex_;
|
||||
|
||||
Preloader preloader_;
|
||||
MediaAsset currentMedia_;
|
||||
MediaAsset nextMedia_;
|
||||
std::string currentMediaPath_;
|
||||
|
||||
std::chrono::steady_clock::time_point mediaStartTime_;
|
||||
AnimationMode configuredAnimationMode_ = AnimationMode::PAN;
|
||||
AnimationMode activeAnimationMode_ = AnimationMode::PAN;
|
||||
long long fadeDurationMs_ = 3000;
|
||||
long long pageTurnDelayMs_ = 5000; // <-- PAGE_TURN 모드 대기 시간 (기본 5초)
|
||||
float animationSpeed_ = 1.0f;
|
||||
|
||||
// std::chrono::steady_clock::time_point transitionStartTime_; // <-- 삭제
|
||||
// bool isTransitioning_ = false; // <-- 삭제
|
||||
// float transitionProgress_ = 0.0f; // <-- 삭제
|
||||
|
||||
std::chrono::steady_clock::time_point fadeStartTime_;
|
||||
std::chrono::steady_clock::time_point pageTurnStartTime_; // <-- PAGE_TURN 대기 시작 시간 추가
|
||||
bool isFading_ = false;
|
||||
|
||||
void drawMedia(ANativeWindow_Buffer& buffer, const MediaAsset& media, float alpha, float offsetElapsedMs);
|
||||
void updateOffset(float& offsetX, float& offsetY, float overflowX, float overflowY, long long elapsedMs);
|
||||
void renderVideoFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha);
|
||||
float currentOffsetX_ = 0.0f;
|
||||
float currentOffsetY_ = 0.0f;
|
||||
int xDirection_ = 1;
|
||||
int yDirection_ = 1;
|
||||
|
||||
float currentZoom_ = 1.0f;
|
||||
int zoomDirection_ = 1;
|
||||
|
||||
bool animationCycleComplete_ = false;
|
||||
|
||||
std::mt19937 randomEngine_;
|
||||
|
||||
void determineActiveAnimationMode();
|
||||
void calculateFitScaleAndOffset(const MediaAsset& media, int surfaceWidth, int surfaceHeight,
|
||||
float& outScale, float& outOffsetX, float& outOffsetY) const;
|
||||
void drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float alpha, float offsetX, float offsetY, float scaleMultiplier);
|
||||
void renderVideoFrame(MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha);
|
||||
void renderImageFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha);
|
||||
};
|
||||
@@ -10,25 +10,29 @@
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
// 전역 변수
|
||||
Renderer* renderer = nullptr;
|
||||
Preloader* preloader = nullptr;
|
||||
// --- ⬇️ 전역 렌더러, 프리로더 포인터 삭제! ⬇️ ---
|
||||
// Renderer* renderer = nullptr;
|
||||
// Preloader* preloader = nullptr;
|
||||
|
||||
// JavaVM과 콜백 관련 전역 변수는 앱 프로세스 전체에서 유일하므로 유지합니다.
|
||||
JavaVM* g_vm = nullptr;
|
||||
jobject g_callback_obj = nullptr;
|
||||
jmethodID g_callback_method_id = nullptr;
|
||||
|
||||
// JNI_OnLoad 함수
|
||||
// JNI_OnLoad, callNextMediaCallback 함수는 기존과 동일
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
g_vm = vm;
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
void callNextMediaCallback() {
|
||||
JNIEnv* env;
|
||||
bool isAttached = false;
|
||||
// 현재 스레드가 JVM에 연결되어 있지 않다면 연결
|
||||
if (g_vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
|
||||
g_vm->AttachCurrentThread(&env, nullptr);
|
||||
if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) {
|
||||
LOGE("Failed to attach current thread to JVM");
|
||||
return;
|
||||
}
|
||||
isAttached = true;
|
||||
}
|
||||
|
||||
@@ -42,22 +46,38 @@ void callNextMediaCallback() {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeInit(JNIEnv* env, jobject) {
|
||||
if (!renderer) {
|
||||
renderer = new Renderer();
|
||||
}
|
||||
if (!preloader) {
|
||||
preloader = new Preloader();
|
||||
}
|
||||
LOGI("Native renderer and preloader initialized.");
|
||||
return JNI_TRUE;
|
||||
// C++ 객체 포인터를 jlong으로, jlong을 C++ 객체 포인터로 안전하게 변환하는 헬퍼 함수
|
||||
template<typename T>
|
||||
T* toNative(jlong handle) {
|
||||
return reinterpret_cast<T*>(handle);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
// [수정] jboolean -> jlong. Renderer 객체를 생성하고 그 주소(핸들)를 반환
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeInit(JNIEnv* env, jobject) {
|
||||
// Preloader는 Renderer가 내부적으로 소유하도록 변경하는 것이 더 좋습니다.
|
||||
// 여기서는 간단하게 Renderer만 생성하여 핸들링합니다.
|
||||
Renderer* renderer = new Renderer();
|
||||
LOGI("Native renderer instance created at address: %p", renderer);
|
||||
return reinterpret_cast<jlong>(renderer);
|
||||
}
|
||||
|
||||
// [수정] 핸들을 파라미터로 받아 해당 객체를 파괴
|
||||
JNIEXPORT void JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeRender(JNIEnv* env, jobject, jobject surface) {
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeDestroy(JNIEnv* env, jobject, jlong nativeHandle) {
|
||||
Renderer* renderer = toNative<Renderer>(nativeHandle);
|
||||
if (renderer) {
|
||||
LOGI("Destroying native renderer instance at address: %p", renderer);
|
||||
delete renderer;
|
||||
}
|
||||
}
|
||||
|
||||
// [수정] 모든 함수가 nativeHandle을 첫 파라미터로 받도록 변경
|
||||
JNIEXPORT void JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeRender(JNIEnv* env, jobject, jlong nativeHandle, jobject surface) {
|
||||
Renderer* renderer = toNative<Renderer>(nativeHandle);
|
||||
if (!renderer || !surface) return;
|
||||
|
||||
ANativeWindow* window = ANativeWindow_fromSurface(env, surface);
|
||||
@@ -65,50 +85,74 @@ Java_bums_lunatic_launcher_wall_NativeRenderer_nativeRender(JNIEnv* env, jobject
|
||||
LOGE("Could not get ANativeWindow from Surface.");
|
||||
return;
|
||||
}
|
||||
|
||||
renderer->renderFrame(window);
|
||||
|
||||
ANativeWindow_release(window);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeDestroy(JNIEnv* env, jobject) {
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeStartNextPreload(JNIEnv* env, jobject, jlong nativeHandle, jint fd) {
|
||||
Renderer* renderer = toNative<Renderer>(nativeHandle);
|
||||
if (renderer) {
|
||||
delete renderer;
|
||||
renderer = nullptr;
|
||||
// Preloader가 Renderer의 일부가 되었다고 가정하고 호출 (추후 Renderer 수정 필요)
|
||||
// 여기서는 간단하게 setNextMedia를 호출하는 것으로 변경합니다.
|
||||
renderer->setNextMedia(fd);
|
||||
}
|
||||
if (preloader) {
|
||||
delete preloader;
|
||||
preloader = nullptr;
|
||||
}
|
||||
if (g_callback_obj) {
|
||||
env->DeleteGlobalRef(g_callback_obj);
|
||||
g_callback_obj = nullptr;
|
||||
}
|
||||
LOGI("Native renderer destroyed and memory freed.");
|
||||
}
|
||||
JNIEXPORT void JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetCurrentMedia(JNIEnv* env, jobject, jint fd) {
|
||||
if (!renderer) return;
|
||||
// 경로 대신 파일 디스크립터를 전달
|
||||
renderer->setNextMedia(fd);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeStartNextPreload(JNIEnv* env, jobject, jint fd) {
|
||||
if (!preloader) return;
|
||||
// 경로 대신 파일 디스크립터를 전달
|
||||
preloader->startNextPreload(fd);
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetAnimationSpeed(JNIEnv* env, jobject, jlong nativeHandle, jfloat speed) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// --- ⬇️ 새로 추가할 JNI 함수들 ⬇️ ---
|
||||
JNIEXPORT void JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetAnimationMode(JNIEnv* env, jobject, jlong nativeHandle, jint mode) {
|
||||
Renderer* renderer = toNative<Renderer>(nativeHandle);
|
||||
if (renderer) {
|
||||
renderer->setAnimationMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetFadeDuration(JNIEnv* env, jobject, jlong nativeHandle, jint duration) {
|
||||
Renderer* renderer = toNative<Renderer>(nativeHandle);
|
||||
if (renderer) {
|
||||
renderer->setFadeDuration(duration);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetPageTurnDelay(JNIEnv* env, jobject, jlong nativeHandle, jint duration) {
|
||||
Renderer* renderer = toNative<Renderer>(nativeHandle);
|
||||
if (renderer) {
|
||||
renderer->setPageTurnDelay(duration);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeGetDebugInfo(JNIEnv* env, jobject, jlong nativeHandle) {
|
||||
Renderer* renderer = toNative<Renderer>(nativeHandle);
|
||||
if (renderer) {
|
||||
std::string debugInfo = renderer->getDebugInfo();
|
||||
return env->NewStringUTF(debugInfo.c_str());
|
||||
}
|
||||
return env->NewStringUTF("Renderer handle is invalid or null.");
|
||||
}
|
||||
|
||||
|
||||
// --- 참고: nativeSetNextMediaCallback은 전역이라 일단 그대로 둡니다 ---
|
||||
JNIEXPORT void JNICALL
|
||||
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetNextMediaCallback(JNIEnv* env, jobject, jobject callback) {
|
||||
if (g_callback_obj) {
|
||||
env->DeleteGlobalRef(g_callback_obj);
|
||||
}
|
||||
g_callback_obj = env->NewGlobalRef(callback);
|
||||
jclass clazz = env->GetObjectClass(callback);
|
||||
g_callback_method_id = env->GetMethodID(clazz, "onNextMediaRequested", "()V");
|
||||
g_callback_obj = env->NewGlobalRef(callback);
|
||||
|
||||
if (!g_callback_method_id) {
|
||||
LOGE("Could not find onNextMediaRequested method ID.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
Reference in New Issue
Block a user