This commit is contained in:
lunaticbum 2025-08-29 16:00:24 +09:00
parent 167c433a59
commit 03086981e5
9 changed files with 581 additions and 448 deletions

View File

@ -1,6 +1,6 @@
#include "AnimationStrategy.h" #include "AnimationStrategy.h"
#include "Renderer.h" // Renderer의 그리기 헬퍼 함수들을 사용하기 위해 포함 #include "Renderer.h"
#include <algorithm> // for std::max, std::clamp #include <algorithm>
#include <vector> #include <vector>
#include <numeric> #include <numeric>
#include <random> #include <random>
@ -13,24 +13,27 @@ public:
PanAnimation(float speed) : AnimationStrategy(speed) { reset(); } PanAnimation(float speed) : AnimationStrategy(speed) { reset(); }
void reset() override { void reset() override {
offsetX_ = 0.0f; state_.offsetX = 0.0f; state_.offsetY = 0.0f;
offsetY_ = 0.0f; state_.scale = 1.0f; cycleComplete_ = false;
cycleComplete_ = false; xDirection_ = -1; // 시작 방향을 -1 (좌로 이동)으로 변경
xDirection_ = 1; yDirection_ = -1; // 시작 방향을 -1 (위로 이동)으로 변경
yDirection_ = 1;
} }
AnimationState getStartState(Renderer* renderer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
AnimationState startState;
startState.offsetX = 0.0f;
startState.offsetY = 0.0f;
startState.scale = getBaseScale(media, surfaceWidth, surfaceHeight);
return startState;
}
AnimationState getState() const override { return state_; }
bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override { bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
// 1. 애니메이션이 이미 끝났으면 더 이상 계산하지 않음 bool cycleComplete = false;
if (cycleComplete_) {
// 마지막 위치에 고정하여 그림
renderer->drawMedia(buffer, media, 1.0f, offsetX_, offsetY_, getBaseScale(media, surfaceWidth, surfaceHeight));
return cycleComplete_;
}
// 2. Pan 모드에 필요한 overflow(화면 밖으로 넘친 영역) 계산
float overflowX = 0.0f, overflowY = 0.0f; float overflowX = 0.0f, overflowY = 0.0f;
float scale = getBaseScale(media, surfaceWidth, surfaceHeight); float scale = getBaseScale(media, surfaceWidth, surfaceHeight);
state_.scale = scale; // 마지막 상태 저장을 위해 현재 스케일값 기록
float mediaW = static_cast<float>(media.getWidth()); float mediaW = static_cast<float>(media.getWidth());
float mediaH = static_cast<float>(media.getHeight()); float mediaH = static_cast<float>(media.getHeight());
if ((mediaW / mediaH) > ((float)surfaceWidth / surfaceHeight)) { if ((mediaW / mediaH) > ((float)surfaceWidth / surfaceHeight)) {
@ -39,38 +42,35 @@ public:
overflowY = std::max(0.0f, mediaH * scale - surfaceHeight); overflowY = std::max(0.0f, mediaH * scale - surfaceHeight);
} }
// 3. 좌표 업데이트 if (!cycleComplete_) {
bool xDone = (overflowX <= 0); bool xDone = (overflowX <= 0);
bool yDone = (overflowY <= 0); bool yDone = (overflowY <= 0);
if (overflowX > 0) { if (overflowX > 0) {
offsetX_ += animationSpeed_ * xDirection_; // offsetX를 음수 방향으로 움직임 (0 -> -overflowX)
if (xDirection_ == 1 && offsetX_ >= overflowX) { offsetX_ = overflowX; xDirection_ = -1; } state_.offsetX += animationSpeed_ * xDirection_;
else if (xDirection_ == -1 && offsetX_ <= 0) { offsetX_ = 0; xDirection_ = 1; xDone = true; } if (xDirection_ == -1 && state_.offsetX <= -overflowX) { state_.offsetX = -overflowX; xDirection_ = 1; }
else if (xDirection_ == 1 && state_.offsetX >= 0) { state_.offsetX = 0; xDirection_ = -1; xDone = true; }
} }
if (overflowY > 0) { if (overflowY > 0) {
offsetY_ += animationSpeed_ * yDirection_; // offsetY를 음수 방향으로 움직임 (0 -> -overflowY)
if (yDirection_ == 1 && offsetY_ >= overflowY) { offsetY_ = overflowY; yDirection_ = -1; } state_.offsetY += animationSpeed_ * yDirection_;
else if (yDirection_ == -1 && offsetY_ <= 0) { offsetY_ = 0; yDirection_ = 1; yDone = true; } if (yDirection_ == -1 && state_.offsetY <= -overflowY) { state_.offsetY = -overflowY; yDirection_ = 1; }
else if (yDirection_ == 1 && state_.offsetY >= 0) { state_.offsetY = 0; yDirection_ = -1; yDone = true; }
}
if (xDone && yDone) cycleComplete_ = true;
} }
// 4. X, Y축 왕복이 모두 끝났는지 확인 renderer->drawMedia(buffer, media, 1.0f, state_.offsetX, state_.offsetY, state_.scale);
if (xDone && yDone) {
cycleComplete_ = true;
}
// 5. 계산된 최종 좌표로 그림
renderer->drawMedia(buffer, media, 1.0f, offsetX_, offsetY_, scale);
return cycleComplete_; return cycleComplete_;
} }
private: private:
float offsetX_, offsetY_; AnimationState state_;
int xDirection_, yDirection_; int xDirection_, yDirection_;
bool cycleComplete_; bool cycleComplete_;
float getBaseScale(MediaAsset& media, int surfaceWidth, int surfaceHeight) { float getBaseScale(MediaAsset& media, int surfaceWidth, int surfaceHeight) {
if (media.getHeight() == 0 || surfaceHeight == 0) return 1.0f;
float scale; float scale;
if ((static_cast<float>(media.getWidth()) / media.getHeight()) > ((float)surfaceWidth / surfaceHeight)) { if ((static_cast<float>(media.getWidth()) / media.getHeight()) > ((float)surfaceWidth / surfaceHeight)) {
scale = (float)surfaceHeight / media.getHeight(); scale = (float)surfaceHeight / media.getHeight();
@ -88,21 +88,21 @@ private:
class PanOneWayAnimation : public AnimationStrategy { class PanOneWayAnimation : public AnimationStrategy {
public: public:
PanOneWayAnimation(float speed) : AnimationStrategy(speed) { reset(); } PanOneWayAnimation(float speed) : AnimationStrategy(speed) { reset(); }
void reset() override { state_ = AnimationState(); cycleComplete_ = false; }
AnimationState getState() const override { return state_; }
void reset() override { AnimationState getStartState(Renderer* renderer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
offsetX_ = 0.0f; AnimationState startState;
offsetY_ = 0.0f; startState.offsetX = 0.0f;
cycleComplete_ = false; startState.offsetY = 0.0f;
startState.scale = getBaseScale(media, surfaceWidth, surfaceHeight);
return startState;
} }
bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override { bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
if (cycleComplete_) {
renderer->drawMedia(buffer, media, 1.0f, offsetX_, offsetY_, getBaseScale(media, surfaceWidth, surfaceHeight));
return cycleComplete_;
}
float overflowX = 0.0f, overflowY = 0.0f; float overflowX = 0.0f, overflowY = 0.0f;
float scale = getBaseScale(media, surfaceWidth, surfaceHeight); float scale = getBaseScale(media, surfaceWidth, surfaceHeight);
state_.scale = scale;
float mediaW = static_cast<float>(media.getWidth()); float mediaW = static_cast<float>(media.getWidth());
float mediaH = static_cast<float>(media.getHeight()); float mediaH = static_cast<float>(media.getHeight());
if ((mediaW / mediaH) > ((float)surfaceWidth / surfaceHeight)) { if ((mediaW / mediaH) > ((float)surfaceWidth / surfaceHeight)) {
@ -110,33 +110,30 @@ public:
} else { } else {
overflowY = std::max(0.0f, mediaH * scale - surfaceHeight); overflowY = std::max(0.0f, mediaH * scale - surfaceHeight);
} }
if (!cycleComplete_) {
bool xReachedEnd = (overflowX <= 0); bool xReachedEnd = (overflowX <= 0);
bool yReachedEnd = (overflowY <= 0); bool yReachedEnd = (overflowY <= 0);
if (overflowX > 0) { if (overflowX > 0) {
offsetX_ += animationSpeed_; // offsetX를 음수 방향으로만 이동
if (offsetX_ >= overflowX) { offsetX_ = overflowX; xReachedEnd = true; } state_.offsetX -= animationSpeed_;
if (state_.offsetX <= -overflowX) { state_.offsetX = -overflowX; xReachedEnd = true; }
} }
if (overflowY > 0) { if (overflowY > 0) {
offsetY_ += animationSpeed_; // offsetY를 음수 방향으로만 이동
if (offsetY_ >= overflowY) { offsetY_ = overflowY; yReachedEnd = true; } state_.offsetY -= animationSpeed_;
if (state_.offsetY <= -overflowY) { state_.offsetY = -overflowY; yReachedEnd = true; }
}
if (xReachedEnd && yReachedEnd) cycleComplete_ = true;
} }
if (xReachedEnd && yReachedEnd) { renderer->drawMedia(buffer, media, 1.0f, state_.offsetX, state_.offsetY, state_.scale);
cycleComplete_ = true;
}
renderer->drawMedia(buffer, media, 1.0f, offsetX_, offsetY_, scale);
return cycleComplete_; return cycleComplete_;
} }
private: private:
float offsetX_, offsetY_; AnimationState state_;
bool cycleComplete_; bool cycleComplete_;
float getBaseScale(MediaAsset& media, int surfaceWidth, int surfaceHeight) { /* PanAnimation과 동일 */
float getBaseScale(MediaAsset& media, int surfaceWidth, int surfaceHeight) { if (media.getHeight() == 0 || surfaceHeight == 0) return 1.0f;
// (PanAnimation과 중복되지만, 각 클래스의 독립성을 위해 포함)
float scale; float scale;
if ((static_cast<float>(media.getWidth()) / media.getHeight()) > ((float)surfaceWidth / surfaceHeight)) { if ((static_cast<float>(media.getWidth()) / media.getHeight()) > ((float)surfaceWidth / surfaceHeight)) {
scale = (float)surfaceHeight / media.getHeight(); scale = (float)surfaceHeight / media.getHeight();
@ -149,35 +146,67 @@ private:
// ==================================================================== // ====================================================================
// --- ZOOM 애니메이션 --- // --- ZOOM 애니메이션 (수정) ---
// ==================================================================== // ====================================================================
class ZoomAnimation : public AnimationStrategy { class ZoomAnimation : public AnimationStrategy {
public: public:
ZoomAnimation(float speed) : AnimationStrategy(speed) { reset(); } ZoomAnimation(float speed) : AnimationStrategy(speed) { reset(); }
void reset() override { void reset() override {
scaleMultiplier_ = 1.0f; state_ = AnimationState();
zoomMultiplier_ = 1.0f; // 줌 배율은 1.0에서 시작
cycleComplete_ = false; cycleComplete_ = false;
zoomDirection_ = 1; zoomDirection_ = 1;
} }
const float zoom_max = 1.35f;
AnimationState getState() const override { return state_; }
AnimationState getStartState(Renderer* renderer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
AnimationState startState;
float baseScale, baseOffsetX, baseOffsetY;
renderer->calculateFitScaleAndOffset(media, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY);
startState.offsetX = baseOffsetX;
startState.offsetY = baseOffsetY;
startState.scale = baseScale * 1.0f; // Zoom의 시작 배율은 1.0
return startState;
}
bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override { bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
if (!cycleComplete_) { if (!cycleComplete_) {
scaleMultiplier_ += 0.0005f * animationSpeed_ * zoomDirection_; // 줌 배율만 업데이트 (1.0 ~ 1.2)
if (zoomDirection_ == 1 && scaleMultiplier_ >= 1.2f) { scaleMultiplier_ = 1.2f; zoomDirection_ = -1; } zoomMultiplier_ += 0.0004f * animationSpeed_ * zoomDirection_;
else if (zoomDirection_ == -1 && scaleMultiplier_ <= 1.0f) { scaleMultiplier_ = 1.0f; zoomDirection_ = 1; cycleComplete_ = true; } if (zoomDirection_ == 1 && zoomMultiplier_ >= zoom_max) {
zoomDirection_ = -1;
} else if (zoomMultiplier_ <= 1.0 && zoomDirection_ == -1){
cycleComplete_ = true; // 최대 확대 시 사이클 완료
}
} }
// ZOOM은 중앙 정렬을 기본으로 함 // 1. 화면에 꽉 차는 기본 스케일을 계산
float baseScale, baseOffsetX, baseOffsetY; float baseScale, baseOffsetX, baseOffsetY;
renderer->calculateFitScaleAndOffset(media, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY); renderer->calculateFitScaleAndOffset(media, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY);
renderer->drawMedia(buffer, media, 1.0f, baseOffsetX, baseOffsetY, baseScale * scaleMultiplier_);
// 2. 최종 스케일은 기본 스케일 * 애니메이션 줌 배율
float finalScale = baseScale * zoomMultiplier_;
// 3. ***변경된 최종 스케일을 바탕으로*** 중앙 정렬 오프셋을 새로 계산
float finalOffsetX = (static_cast<float>(surfaceWidth) - (media.getWidth() * finalScale)) / 2.0f;
float finalOffsetY = (static_cast<float>(surfaceHeight) - (media.getHeight() * finalScale)) / 2.0f;
// 4. getState()가 올바른 마지막 상태를 보고할 수 있도록 state_에 최종 상태 저장
state_.offsetX = finalOffsetX;
state_.offsetY = finalOffsetY;
state_.scale = finalScale;
// 5. 최종 계산된 값으로 그리기
renderer->drawMedia(buffer, media, 1.0f, state_.offsetX, state_.offsetY, state_.scale);
return cycleComplete_; return cycleComplete_;
} }
private: private:
float scaleMultiplier_; AnimationState state_;
float zoomMultiplier_; // 애니메이션 배율만 따로 관리
int zoomDirection_; int zoomDirection_;
bool cycleComplete_; bool cycleComplete_;
}; };
@ -191,10 +220,23 @@ public:
PageTurnAnimation(float speed, long long delay) : AnimationStrategy(speed), delayMs_(delay) { reset(); } PageTurnAnimation(float speed, long long delay) : AnimationStrategy(speed), delayMs_(delay) { reset(); }
void reset() override { void reset() override {
state_ = AnimationState();
cycleComplete_ = false; cycleComplete_ = false;
startTime_ = std::chrono::steady_clock::now(); startTime_ = std::chrono::steady_clock::now();
} }
AnimationState getStartState(Renderer* renderer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
AnimationState startState;
float baseScale, baseOffsetX, baseOffsetY;
renderer->calculateFitScaleAndOffset(media, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY);
startState.offsetX = baseOffsetX;
startState.offsetY = baseOffsetY;
startState.scale = baseScale * 1.0f; // Zoom의 시작 배율은 1.0
return startState;
}
AnimationState getState() const override { return state_; }
bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override { bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
if (!cycleComplete_) { if (!cycleComplete_) {
long long elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - startTime_).count(); long long elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - startTime_).count();
@ -203,14 +245,17 @@ public:
} }
} }
// 대기하는 동안 중앙에 고정된 이미지를 그림
float baseScale, baseOffsetX, baseOffsetY; float baseScale, baseOffsetX, baseOffsetY;
renderer->calculateFitScaleAndOffset(media, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY); renderer->calculateFitScaleAndOffset(media, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY);
renderer->drawMedia(buffer, media, 1.0f, baseOffsetX, baseOffsetY, baseScale); state_.offsetX = baseOffsetX;
state_.offsetY = baseOffsetY;
state_.scale = baseScale;
renderer->drawMedia(buffer, media, 1.0f, state_.offsetX, state_.offsetY, state_.scale);
return cycleComplete_; return cycleComplete_;
} }
private: private:
AnimationState state_;
long long delayMs_; long long delayMs_;
std::chrono::steady_clock::time_point startTime_; std::chrono::steady_clock::time_point startTime_;
bool cycleComplete_; bool cycleComplete_;
@ -222,15 +267,31 @@ private:
// ==================================================================== // ====================================================================
class NoneAnimation : public AnimationStrategy { class NoneAnimation : public AnimationStrategy {
public: public:
NoneAnimation(float speed) : AnimationStrategy(speed) {} NoneAnimation(float speed) : AnimationStrategy(speed) { reset(); }
void reset() override {} // 아무것도 안 함 void reset() override { state_ = AnimationState();
callCount_ = 0;}
AnimationState getState() const override { return state_; }
bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override { AnimationState getStartState(Renderer* renderer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
// 중앙에 고정된 이미지만 그림 AnimationState startState;
float baseScale, baseOffsetX, baseOffsetY; float baseScale, baseOffsetX, baseOffsetY;
renderer->calculateFitScaleAndOffset(media, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY); renderer->calculateFitScaleAndOffset(media, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY);
renderer->drawMedia(buffer, media, 1.0f, baseOffsetX, baseOffsetY, baseScale); startState.offsetX = baseOffsetX;
startState.offsetY = baseOffsetY;
return true; // 즉시 완료 startState.scale = baseScale * 1.0f; // Zoom의 시작 배율은 1.0
return startState;
} }
bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
float baseScale, baseOffsetX, baseOffsetY;
renderer->calculateFitScaleAndOffset(media, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY);
state_.offsetX = baseOffsetX;
state_.offsetY = baseOffsetY;
state_.scale = baseScale;
renderer->drawMedia(buffer, media, 1.0f, state_.offsetX, state_.offsetY, state_.scale);
return callCount_++ > 120;
}
private:
int callCount_;
AnimationState state_;
}; };

View File

@ -2,21 +2,47 @@
#include <android/native_window.h> #include <android/native_window.h>
#include "MediaAsset.h" #include "MediaAsset.h"
class Renderer; // 전방 선언 // 전방 선언: 순환 참조를 방지하기 위해 클래스의 이름만 먼저 알려줍니다.
class Renderer;
/**
* @brief () .
* Renderer는 .
*/
struct AnimationState {
float offsetX = 0.0f;
float offsetY = 0.0f;
float scale = 1.0f;
};
/**
* @brief (Strategy) ( ).
*/
class AnimationStrategy { class AnimationStrategy {
public: public:
virtual ~AnimationStrategy() = default; virtual ~AnimationStrategy() = default;
/** /**
* @brief . * @brief , .
* @return true * @return true를 .
*/ */
virtual bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) = 0; virtual bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) = 0;
/**
* @brief .
*/
virtual void reset() = 0; virtual void reset() = 0;
/**
* @brief .
*/
virtual AnimationState getState() const = 0;
// --- ⬇️ 자신의 시작 상태를 알려주는 함수 추가 ⬇️ ---
virtual AnimationState getStartState(Renderer* renderer, MediaAsset& media, int surfaceWidth, int surfaceHeight) = 0;
protected: protected:
// 생성자에서 애니메이션 속도를 받아 저장합니다.
AnimationStrategy(float speed) : animationSpeed_(speed) {} AnimationStrategy(float speed) : animationSpeed_(speed) {}
float animationSpeed_; float animationSpeed_;
}; };

View File

@ -12,171 +12,279 @@
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// MediaAsset::MediaAsset() = default; // .cpp 파일에서 삭제 // --- ⬇️ FFmpeg Custom I/O를 위한 헬퍼 함수들 ⬇️ ---
// FFmpeg이 데이터를 요청할 때 호출될 읽기 함수
static int read_packet(void *opaque, uint8_t *buf, int buf_size) {
FILE* file = (FILE*)opaque;
if (feof(file)) {
return AVERROR_EOF;
}
size_t bytes_read = fread(buf, 1, buf_size, file);
if (bytes_read < buf_size) {
if (ferror(file)) {
return AVERROR(EIO);
}
}
return bytes_read;
}
// FFmpeg이 위치 이동을 요청할 때 호출될 탐색 함수
static int64_t seek_packet(void *opaque, int64_t offset, int whence) {
FILE* file = (FILE*)opaque;
// SEEK_SET, SEEK_CUR, SEEK_END, AVSEEK_SIZE
if (whence == AVSEEK_SIZE) {
long current_pos = ftell(file);
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, current_pos, SEEK_SET);
return size;
}
if (fseek(file, offset, whence) != 0) {
return -1;
}
return ftell(file);
}
// --- 헬퍼 함수 끝 ---
MediaAsset::MediaAsset(MediaAsset&& other) noexcept
: type_(other.type_), width_(other.width_), height_(other.height_),
imageData_(other.imageData_), rgbBuffer_(std::move(other.rgbBuffer_)),
fmtCtx_(other.fmtCtx_), codecCtx_(other.codecCtx_), frame_(other.frame_),
packet_(other.packet_), swsCtx_(other.swsCtx_), videoStreamIdx_(other.videoStreamIdx_) {
other.type_ = Type::UNKNOWN;
other.imageData_ = nullptr;
other.fmtCtx_ = nullptr;
other.codecCtx_ = nullptr;
other.frame_ = nullptr;
other.packet_ = nullptr;
other.swsCtx_ = nullptr;
}
MediaAsset& MediaAsset::operator=(MediaAsset&& other) noexcept {
if (this != &other) {
release();
type_ = other.type_;
width_ = other.width_;
height_ = other.height_;
imageData_ = other.imageData_;
rgbBuffer_ = std::move(other.rgbBuffer_);
fmtCtx_ = other.fmtCtx_;
codecCtx_ = other.codecCtx_;
frame_ = other.frame_;
packet_ = other.packet_;
swsCtx_ = other.swsCtx_;
videoStreamIdx_ = other.videoStreamIdx_;
other.type_ = Type::UNKNOWN;
other.imageData_ = nullptr;
other.fmtCtx_ = nullptr;
other.codecCtx_ = nullptr;
other.frame_ = nullptr;
other.packet_ = nullptr;
other.swsCtx_ = nullptr;
}
return *this;
}
MediaAsset::~MediaAsset() { MediaAsset::~MediaAsset() {
release(); release();
} }
// [수정] 파일 디스크립터를 받는 load 함수 구현 // --- ⬇️ load(int fd) 함수 수정 ⬇️ ---
bool MediaAsset::load(int fd) { bool MediaAsset::load(int fd) {
if (fd < 0) { if (fd < 0) {
LOGE("Load failed: Invalid file descriptor."); LOGE("Load failed: Invalid file descriptor.");
return false; 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(); release();
if (path.empty()) {
LOGE("Load failed: Path is empty."); // 먼저 비디오로 로딩 시도 (Custom I/O 방식)
if (loadVideoWithFFmpeg(fd)) {
return true;
}
// 비디오 실패 시, 이미지로 로딩 시도 (/proc 경로 방식)
LOGI("Video load failed for fd:%d, trying as image.", fd);
std::string image_path = "/proc/self/fd/" + std::to_string(fd);
if (loadImageWithStb(image_path)) {
// 이미지 로딩 성공 시에는 fd를 여기서 닫아줌
close(fd);
return true;
}
LOGE("Failed to load media from fd: %d", fd);
close(fd); // 모든 로딩 실패 시에도 fd를 닫아줌
return false; return false;
} }
// path 기반 load 함수는 그대로 유지
bool MediaAsset::load(const std::string& path) {
release();
size_t dotPos = path.find_last_of('.'); size_t dotPos = path.find_last_of('.');
// '/proc/self/fd/...' 경로처럼 확장자가 없는 경우
if (dotPos == std::string::npos) { if (dotPos == std::string::npos) {
LOGI("No extension found for %s. Attempting smart load.", path.c_str()); LOGE("Load failed: Could not find file extension in path: %s", path.c_str());
// 비디오로 먼저 시도하고, 성공하면 즉시 반환 return false;
if (loadVideoWithFFmpeg(path)) {
return true;
} }
// 비디오 로딩 실패 시, 이미지로 이어서 시도
LOGI("Video load failed, trying as image.");
return loadImageWithStb(path);
}
// 확장자가 있는 일반 경로의 경우
std::string extension = path.substr(dotPos + 1); std::string extension = path.substr(dotPos + 1);
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
const std::vector<std::string> videoExts = {"mp4", "mkv", "avi", "mov", "webm","gif"};
const std::vector<std::string> videoExts = {"mp4", "mkv", "avi", "mov", "webm"};
const std::vector<std::string> imageExts = {"jpg", "jpeg", "png", "bmp", "webp"}; const std::vector<std::string> imageExts = {"jpg", "jpeg", "png", "bmp", "webp"};
if (std::find(imageExts.begin(), imageExts.end(), extension) != imageExts.end()) {
bool isVideo = std::find(videoExts.begin(), videoExts.end(), extension) != videoExts.end();
bool isImage = std::find(imageExts.begin(), imageExts.end(), extension) != imageExts.end();
if (isVideo) { // [수정] 비디오 확장자일 경우 비디오만 시도
return loadVideoWithFFmpeg(path);
} else if (isImage) { // [수정] 이미지 확장자일 경우 이미지만 시도
return loadImageWithStb(path); return loadImageWithStb(path);
} else if (std::find(videoExts.begin(), videoExts.end(), extension) != videoExts.end()) {
// path 기반 비디오 로딩은 이제 fd 기반 로딩을 호출하지 않음
return loadVideoWithFFmpeg(path.c_str());
} }
LOGE("Load failed: Unsupported file format for path: %s", path.c_str()); LOGE("Load failed: Unsupported file format for path: %s", path.c_str());
return false; return false;
} }
// stb_image.h를 사용하여 이미지를 로드하는 함수
bool MediaAsset::loadImageWithStb(const std::string& path) { bool MediaAsset::loadImageWithStb(const std::string& path) {
type_ = Type::IMAGE; type_ = Type::IMAGE;
// 4 채널(RGBA)로 강제 변환하여 로드합니다.
imageData_ = stbi_load(path.c_str(), &width_, &height_, nullptr, 4); imageData_ = stbi_load(path.c_str(), &width_, &height_, nullptr, 4);
if (!imageData_) { if (!imageData_) {
LOGE("Failed to load image with stb_image: %s", stbi_failure_reason()); LOGE("Failed to load image with stb_image: %s", stbi_failure_reason());
return false; return false;
} }
LOGI("Successfully loaded image with stb_image: %s (%dx%d)", path.c_str(), width_, height_); LOGI("Successfully loaded image: %s (%dx%d)", path.c_str(), width_, height_);
return true; return true;
} }
// FFmpeg을 사용하여 비디오를 로드하는 함수
bool MediaAsset::loadVideoWithFFmpeg(const std::string& path) { // --- ⬇️ loadVideoWithFFmpeg 함수를 오버로딩하여 Custom I/O 로직 추가 ⬇️ ---
bool MediaAsset::loadVideoWithFFmpeg(int fd) {
type_ = Type::VIDEO; type_ = Type::VIDEO;
// 1. 비디오 파일 열기 // 1. fd를 FILE* 스트림으로 변환
if (avformat_open_input(&fmtCtx_, path.c_str(), nullptr, nullptr) != 0) { FILE* file = fdopen(fd, "rb");
LOGE("Could not open video file: %s", path.c_str()); // 오타 수정: c_st() -> c_str() if (!file) {
release(); LOGE("Custom I/O: Failed to fdopen for fd: %d", fd);
close(fd); // fdopen 실패 시 fd를 직접 닫음
return false; return false;
} }
// 2. 스트림 정보 찾기 // 2. FFmpeg Custom I/O를 위한 버퍼 및 컨텍스트 생성
const int buffer_size = 4096;
unsigned char* buffer = (unsigned char*)av_malloc(buffer_size);
AVIOContext* ioCtx = avio_alloc_context(
buffer, buffer_size, 0, file, &read_packet, nullptr, &seek_packet
);
if (!ioCtx) {
LOGE("Custom I/O: Failed to avio_alloc_context");
fclose(file);
av_free(buffer);
return false;
}
fmtCtx_ = avformat_alloc_context();
fmtCtx_->pb = ioCtx; // 생성한 I/O 컨텍스트를 포맷 컨텍스트에 연결
// 3. avformat_open_input 호출 (첫 번째 인자로 nullptr 전달)
if (avformat_open_input(&fmtCtx_, nullptr, nullptr, nullptr) != 0) {
LOGE("Custom I/O: avformat_open_input failed");
// 실패 시 자원 정리 (fmtCtx_는 여기서 해제하면 안됨)
avio_context_free(&fmtCtx_->pb); // fmtCtx_가 소유권을 가져갔으므로 이렇게 해제
fmtCtx_ = nullptr;
// fclose는 이미 ioCtx가 관리하므로 호출 불필요
return false;
}
// --- 이후 로직은 기존과 동일 ---
if (avformat_find_stream_info(fmtCtx_, nullptr) < 0) { if (avformat_find_stream_info(fmtCtx_, nullptr) < 0) {
LOGE("Could not find stream information for %s", path.c_str()); LOGE("Could not find stream information");
release(); release(); return false;
return false;
} }
// 3. 최적의 비디오 스트림 찾기
const AVCodec* codec = nullptr; const AVCodec* codec = nullptr;
videoStreamIdx_ = av_find_best_stream(fmtCtx_, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0); videoStreamIdx_ = av_find_best_stream(fmtCtx_, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
if (videoStreamIdx_ < 0) { if (videoStreamIdx_ < 0) {
LOGE("Could not find a video stream in %s", path.c_str()); LOGE("Could not find a video stream");
release(); release(); return false;
return false;
} }
// 4. 코덱 컨텍스트 준비
codecCtx_ = avcodec_alloc_context3(codec); codecCtx_ = avcodec_alloc_context3(codec);
if (!codecCtx_ || avcodec_parameters_to_context(codecCtx_, fmtCtx_->streams[videoStreamIdx_]->codecpar) < 0) { if (!codecCtx_ || avcodec_parameters_to_context(codecCtx_, fmtCtx_->streams[videoStreamIdx_]->codecpar) < 0) {
LOGE("Failed to create codec context for %s", path.c_str()); LOGE("Failed to create codec context");
release(); release(); return false;
return false;
} }
// 5. 코덱 열기
if (avcodec_open2(codecCtx_, codec, nullptr) < 0) { if (avcodec_open2(codecCtx_, codec, nullptr) < 0) {
LOGE("Could not open codec for %s", path.c_str()); LOGE("Could not open codec");
release(); release(); return false;
return false;
} }
// 6. 프레임 및 패킷 할당
frame_ = av_frame_alloc(); frame_ = av_frame_alloc();
packet_ = av_packet_alloc(); packet_ = av_packet_alloc();
if (!frame_ || !packet_) { if (!frame_ || !packet_) {
LOGE("Could not allocate frame or packet for %s", path.c_str()); LOGE("Could not allocate frame or packet");
release(); release(); return false;
return false;
} }
// 7. RGBA 변환을 위한 SwsContext 준비
width_ = codecCtx_->width; width_ = codecCtx_->width;
height_ = codecCtx_->height; height_ = codecCtx_->height;
swsCtx_ = sws_getContext( swsCtx_ = sws_getContext(width_, height_, codecCtx_->pix_fmt, width_, height_, AV_PIX_FMT_RGBA, SWS_BILINEAR, nullptr, nullptr, nullptr);
width_, height_, codecCtx_->pix_fmt,
width_, height_, AV_PIX_FMT_RGBA,
SWS_BILINEAR, nullptr, nullptr, nullptr
);
if (!swsCtx_) { if (!swsCtx_) {
LOGE("Could not create SwsContext for %s", path.c_str()); LOGE("Could not create SwsContext");
release(); release(); return false;
return false;
} }
// 8. RGBA 픽셀 데이터를 담을 버퍼 할당
rgbBuffer_.resize(width_ * height_ * 4); rgbBuffer_.resize(width_ * height_ * 4);
LOGI("Successfully loaded video: %s (W: %d, H: %d)", path.c_str(), width_, height_); LOGI("Successfully loaded video via Custom I/O from fd: %d", fd);
return true; return true;
} }
// 기존의 path 기반 비디오 로딩 함수 (이제 거의 사용되지 않음)
bool MediaAsset::loadVideoWithFFmpeg(const std::string& path) {
type_ = Type::VIDEO;
if (avformat_open_input(&fmtCtx_, path.c_str(), nullptr, nullptr) != 0) {
LOGE("Could not open video file: %s", path.c_str());
release(); return false;
}
// ... (이하 로직은 Custom I/O 버전과 동일)
if (avformat_find_stream_info(fmtCtx_, nullptr) < 0) { release(); return false; }
const AVCodec* codec = nullptr;
videoStreamIdx_ = av_find_best_stream(fmtCtx_, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
if (videoStreamIdx_ < 0) { release(); return false; }
codecCtx_ = avcodec_alloc_context3(codec);
if (!codecCtx_ || avcodec_parameters_to_context(codecCtx_, fmtCtx_->streams[videoStreamIdx_]->codecpar) < 0) { release(); return false; }
if (avcodec_open2(codecCtx_, codec, nullptr) < 0) { release(); return false; }
frame_ = av_frame_alloc(); packet_ = av_packet_alloc();
if (!frame_ || !packet_) { release(); return false; }
width_ = codecCtx_->width; height_ = codecCtx_->height;
swsCtx_ = sws_getContext(width_, height_, codecCtx_->pix_fmt, width_, height_, AV_PIX_FMT_RGBA, SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!swsCtx_) { release(); return false; }
rgbBuffer_.resize(width_ * height_ * 4);
LOGI("Successfully loaded video from path: %s", path.c_str());
return true;
}
void MediaAsset::release() { void MediaAsset::release() {
// 이미지 데이터 해제
if (type_ == Type::IMAGE && imageData_) { if (type_ == Type::IMAGE && imageData_) {
stbi_image_free(imageData_); stbi_image_free(imageData_);
} }
imageData_ = nullptr; imageData_ = nullptr;
// FFmpeg 비디오 자원 해제
if (packet_) av_packet_free(&packet_); if (packet_) av_packet_free(&packet_);
if (frame_) av_frame_free(&frame_); if (frame_) av_frame_free(&frame_);
if (codecCtx_) avcodec_close(codecCtx_); // avcodec_free_context 전에 호출 // if (codecCtx_) avcodec_close(codecCtx_);
// if (codecCtx_) avcodec_free_context(&codecCtx_);
if (codecCtx_) avcodec_free_context(&codecCtx_); if (codecCtx_) avcodec_free_context(&codecCtx_);
if (fmtCtx_) avformat_close_input(&fmtCtx_);
if (fmtCtx_) {
// Custom I/O로 열었을 경우, IO 컨텍스트의 버퍼를 직접 해제해주어야 함
if (fmtCtx_->pb) {
if (fmtCtx_->pb->buffer) {
av_free(fmtCtx_->pb->buffer);
fmtCtx_->pb->buffer = nullptr;
}
// FILE*는 avio_context_free가 닫아주지 않으므로, 우리가 직접 닫아야 함
if (fmtCtx_->pb->opaque) {
fclose((FILE*)fmtCtx_->pb->opaque);
fmtCtx_->pb->opaque = nullptr;
}
avio_context_free(&fmtCtx_->pb);
}
avformat_close_input(&fmtCtx_);
}
if (swsCtx_) sws_freeContext(swsCtx_); if (swsCtx_) sws_freeContext(swsCtx_);
packet_ = nullptr; packet_ = nullptr;
@ -184,8 +292,6 @@ void MediaAsset::release() {
codecCtx_ = nullptr; codecCtx_ = nullptr;
fmtCtx_ = nullptr; fmtCtx_ = nullptr;
swsCtx_ = nullptr; swsCtx_ = nullptr;
// 공통 변수 초기화
type_ = Type::UNKNOWN; type_ = Type::UNKNOWN;
width_ = 0; width_ = 0;
height_ = 0; height_ = 0;
@ -201,50 +307,3 @@ bool MediaAsset::isValid() const {
} }
return false; return false;
} }
// --- 이동 생성자 및 이동 할당 연산자 (자원 소유권 이전) ---
MediaAsset::MediaAsset(MediaAsset&& other) noexcept
: type_(other.type_), width_(other.width_), height_(other.height_),
imageData_(other.imageData_), rgbBuffer_(std::move(other.rgbBuffer_)),
fmtCtx_(other.fmtCtx_), codecCtx_(other.codecCtx_), frame_(other.frame_),
packet_(other.packet_), swsCtx_(other.swsCtx_), videoStreamIdx_(other.videoStreamIdx_) {
// 소유권을 이전했으므로, 원본 객체의 포인터들은 초기화하여 이중 해제를 방지
other.type_ = Type::UNKNOWN;
other.imageData_ = nullptr;
other.fmtCtx_ = nullptr;
other.codecCtx_ = nullptr;
other.frame_ = nullptr;
other.packet_ = nullptr;
other.swsCtx_ = nullptr;
}
MediaAsset& MediaAsset::operator=(MediaAsset&& other) noexcept {
if (this != &other) {
release(); // 기존 자원 정리
// 자원 소유권 이전
type_ = other.type_;
width_ = other.width_;
height_ = other.height_;
imageData_ = other.imageData_;
rgbBuffer_ = std::move(other.rgbBuffer_);
fmtCtx_ = other.fmtCtx_;
codecCtx_ = other.codecCtx_;
frame_ = other.frame_;
packet_ = other.packet_;
swsCtx_ = other.swsCtx_;
videoStreamIdx_ = other.videoStreamIdx_;
// 원본 객체 초기화
other.type_ = Type::UNKNOWN;
other.imageData_ = nullptr;
other.fmtCtx_ = nullptr;
other.codecCtx_ = nullptr;
other.frame_ = nullptr;
other.packet_ = nullptr;
other.swsCtx_ = nullptr;
}
return *this;
}

View File

@ -21,31 +21,30 @@ public:
MediaAsset() = default; MediaAsset() = default;
~MediaAsset(); ~MediaAsset();
// 복사를 방지하고 자원의 소유권을 효율적으로 이전하기 위한 이동 생성자 및 연산자
MediaAsset(MediaAsset&& other) noexcept; MediaAsset(MediaAsset&& other) noexcept;
MediaAsset& operator=(MediaAsset&& other) noexcept; MediaAsset& operator=(MediaAsset&& other) noexcept;
// 파일을 로드하는 메인 함수 (오버로딩)
bool load(const std::string& path); bool load(const std::string& path);
bool load(int fd); bool load(int fd);
// 모든 자원을 해제하는 함수
void release(); void release();
// 현재 미디어가 유효한지 확인
bool isValid() const; bool isValid() const;
// Getter 함수들
Type getType() const { return type_; } Type getType() const { return type_; }
int getWidth() const { return width_; } int getWidth() const { return width_; }
int getHeight() const { return height_; } int getHeight() const { return height_; }
// --- 수정된 부분 ---
// 기존: uint8_t* getImageData() const { return imageData_; }
// const 객체는 const 포인터를 반환하도록 수정
const uint8_t* getImageData() const { return imageData_; } const uint8_t* getImageData() const { return imageData_; }
// 기존: std::vector<uint8_t>& getRgbBuffer() { return rgbBuffer_; }
// non-const 객체용 (기존 함수 유지)
std::vector<uint8_t>& getRgbBuffer() { return rgbBuffer_; } std::vector<uint8_t>& getRgbBuffer() { return rgbBuffer_; }
// const 객체용 오버로딩 함수 추가
const std::vector<uint8_t>& getRgbBuffer() const { return rgbBuffer_; } const std::vector<uint8_t>& getRgbBuffer() const { return rgbBuffer_; }
// --- 수정 끝 ---
// FFmpeg 비디오 처리를 위한 Getter 함수들
AVFormatContext* getFormatContext() const { return fmtCtx_; } AVFormatContext* getFormatContext() const { return fmtCtx_; }
AVCodecContext* getCodecContext() const { return codecCtx_; } AVCodecContext* getCodecContext() const { return codecCtx_; }
AVFrame* getFrame() const { return frame_; } AVFrame* getFrame() const { return frame_; }
@ -54,20 +53,23 @@ public:
int getVideoStreamIndex() const { return videoStreamIdx_; } int getVideoStreamIndex() const { return videoStreamIdx_; }
private: private:
// ... (이하 동일)
bool loadInternal(const std::string& path); bool loadInternal(const std::string& path);
// --- ⬇️ 수정된 부분: int fd를 받는 함수 선언 추가 ⬇️ ---
bool loadVideoWithFFmpeg(const std::string& path); bool loadVideoWithFFmpeg(const std::string& path);
bool loadVideoWithFFmpeg(int fd); // Custom I/O를 위한 오버로딩
bool loadImageWithStb(const std::string& path); bool loadImageWithStb(const std::string& path);
// --- 수정 끝 ---
Type type_ = Type::UNKNOWN; Type type_ = Type::UNKNOWN;
int width_ = 0; int width_ = 0;
int height_ = 0; int height_ = 0;
uint8_t* imageData_ = nullptr; uint8_t* imageData_ = nullptr;
std::vector<uint8_t> rgbBuffer_;
AVFormatContext* fmtCtx_ = nullptr; AVFormatContext* fmtCtx_ = nullptr;
AVCodecContext* codecCtx_ = nullptr; AVCodecContext* codecCtx_ = nullptr;
AVFrame* frame_ = nullptr; AVFrame* frame_ = nullptr;
AVPacket* packet_ = nullptr; AVPacket* packet_ = nullptr;
SwsContext* swsCtx_ = nullptr; SwsContext* swsCtx_ = nullptr;
int videoStreamIdx_ = -1; int videoStreamIdx_ = -1;
std::vector<uint8_t> rgbBuffer_;
}; };

View File

@ -1,5 +1,6 @@
#include "Renderer.h" #include "Renderer.h"
#include "AnimationStrategy.cpp" #include "AnimationStrategy.cpp"
#include "AnimationStrategy.h"
#include "TransitionStrategy.cpp" #include "TransitionStrategy.cpp"
#include <android/log.h> #include <android/log.h>
#include <algorithm> #include <algorithm>
@ -16,85 +17,42 @@ extern void callNextMediaCallback();
// ==================================================================== // ====================================================================
// 생성자, 소멸자 및 설정(Setter) 함수들 // 생성자, 소멸자 및 설정(Setter) 함수들
// ==================================================================== // ====================================================================
Renderer::Renderer() { Renderer::Renderer() {
randomEngine_.seed(std::chrono::high_resolution_clock::now().time_since_epoch().count()); randomEngine_.seed(std::chrono::high_resolution_clock::now().time_since_epoch().count());
setAnimationMode(static_cast<int>(AnimationMode::PAN)); setAnimationMode(static_cast<int>(AnimationMode::PAN));
setTransitionMode(static_cast<int>(TransitionMode::FADE)); setTransitionMode(static_cast<int>(TransitionMode::FADE));
} }
Renderer::~Renderer() { release(); }
Renderer::~Renderer() { void Renderer::release() { std::lock_guard<std::mutex> lock(renderMutex_); currentMedia_.release(); nextMedia_.release(); }
release(); void Renderer::setNextMedia(int fd) { preloader_.startNextPreload(fd); }
} void Renderer::setAnimationSpeed(float speed) { animationSpeed_ = speed > 0 ? speed : 1.0f; }
void Renderer::setFadeDuration(int durationMs) { fadeDurationMs_ = durationMs > 0 ? durationMs : 3000; }
void Renderer::release() { void Renderer::setPageTurnDelay(int delayMs) { pageTurnDelayMs_ = delayMs > 0 ? delayMs : 5000; }
std::lock_guard<std::mutex> lock(renderMutex_);
currentMedia_.release();
nextMedia_.release();
}
void Renderer::setNextMedia(int fd) {
preloader_.startNextPreload(fd);
}
void Renderer::setAnimationSpeed(float speed) {
animationSpeed_ = speed > 0 ? speed : 1.0f;
}
void Renderer::setFadeDuration(int durationMs) {
fadeDurationMs_ = durationMs > 0 ? durationMs : 3000;
}
void Renderer::setPageTurnDelay(int delayMs) {
pageTurnDelayMs_ = delayMs > 0 ? delayMs : 5000;
}
void Renderer::setAnimationMode(int mode) { void Renderer::setAnimationMode(int mode) {
configuredAnimationMode_ = (mode >= 0 && mode <= static_cast<int>(AnimationMode::PAGE_TURN)) configuredAnimationMode_ = (mode >= 0 && mode <= static_cast<int>(AnimationMode::PAGE_TURN)) ? static_cast<AnimationMode>(mode) : AnimationMode::PAN;
? static_cast<AnimationMode>(mode)
: AnimationMode::PAN;
determineActiveAnimationMode(); determineActiveAnimationMode();
} }
void Renderer::setTransitionMode(int mode) { void Renderer::setTransitionMode(int mode) {
configuredTransitionMode_ = (mode >= 0 && mode <= static_cast<int>(TransitionMode::MOSAIC)) configuredTransitionMode_ = (mode >= 0 && mode <= static_cast<int>(TransitionMode::MOSAIC)) ? static_cast<TransitionMode>(mode) : TransitionMode::FADE;
? static_cast<TransitionMode>(mode)
: TransitionMode::FADE;
} }
void Renderer::determineActiveAnimationMode() { void Renderer::determineActiveAnimationMode() {
AnimationMode modeToSetActive; AnimationMode modeToSetActive;
if (configuredAnimationMode_ == AnimationMode::RANDOM) { if (configuredAnimationMode_ == AnimationMode::RANDOM) {
static const std::vector<AnimationMode> availableModes = { static const std::vector<AnimationMode> availableModes = { AnimationMode::PAN, AnimationMode::ZOOM, AnimationMode::NONE, AnimationMode::PAN_ONE_WAY, AnimationMode::PAGE_TURN };
AnimationMode::PAN, AnimationMode::ZOOM, AnimationMode::NONE,
AnimationMode::PAN_ONE_WAY, AnimationMode::PAGE_TURN
};
std::uniform_int_distribution<size_t> dist(0, availableModes.size() - 1); std::uniform_int_distribution<size_t> dist(0, availableModes.size() - 1);
modeToSetActive = availableModes[dist(randomEngine_)]; modeToSetActive = availableModes[dist(randomEngine_)];
} else { } else {
modeToSetActive = configuredAnimationMode_; modeToSetActive = configuredAnimationMode_;
} }
activeAnimationMode_ = modeToSetActive; activeAnimationMode_ = modeToSetActive;
switch (activeAnimationMode_) { switch (activeAnimationMode_) {
case AnimationMode::PAN: case AnimationMode::PAN: animationStrategy_ = std::make_unique<PanAnimation>(animationSpeed_); break;
animationStrategy_ = std::make_unique<PanAnimation>(animationSpeed_); case AnimationMode::PAN_ONE_WAY: animationStrategy_ = std::make_unique<PanOneWayAnimation>(animationSpeed_); break;
break; case AnimationMode::ZOOM: animationStrategy_ = std::make_unique<ZoomAnimation>(animationSpeed_); break;
case AnimationMode::PAN_ONE_WAY: case AnimationMode::PAGE_TURN: animationStrategy_ = std::make_unique<PageTurnAnimation>(animationSpeed_, pageTurnDelayMs_); break;
animationStrategy_ = std::make_unique<PanOneWayAnimation>(animationSpeed_); case AnimationMode::NONE: default: animationStrategy_ = std::make_unique<NoneAnimation>(animationSpeed_); break;
break;
case AnimationMode::ZOOM:
animationStrategy_ = std::make_unique<NoneAnimation>(animationSpeed_);
break;
case AnimationMode::PAGE_TURN:
animationStrategy_ = std::make_unique<PageTurnAnimation>(animationSpeed_, pageTurnDelayMs_);
break;
case AnimationMode::NONE:
default:
animationStrategy_ = std::make_unique<NoneAnimation>(animationSpeed_);
break;
case AnimationMode::RANDOM:
break;
} }
} }
@ -103,68 +61,133 @@ void Renderer::determineActiveAnimationMode() {
// ==================================================================== // ====================================================================
void Renderer::handleAnimationState(ANativeWindow_Buffer& buffer, int surfaceWidth, int surfaceHeight) { void Renderer::handleAnimationState(ANativeWindow_Buffer& buffer, int surfaceWidth, int surfaceHeight) {
// 안전장치: 애니메이션 전문가가 고용되지 않았다면 작업을 중단 if (!animationStrategy_) return;
if (!animationStrategy_) {
// 이 경우엔 그냥 검은 화면만 그림
memset(buffer.bits, 0, buffer.stride * buffer.height * sizeof(uint32_t));
return;
}
// 1. 위임: 현재 애니메이션 전문가에게 모든 계산과 그리기를 맡기고, 완료 여부만 보고받음
// (이 execute 함수 내부에서 배경을 지우고, 좌표를 계산하고, drawMedia를 호출하는 모든 작업을 수행함)
bool isCycleComplete = animationStrategy_->execute(this, buffer, currentMedia_, surfaceWidth, surfaceHeight); bool isCycleComplete = animationStrategy_->execute(this, buffer, currentMedia_, surfaceWidth, surfaceHeight);
// 2. 상태 전환: 애니메이션이 끝났고, 다음 미디어가 대기 중이라면 '전환' 상태로 넘어감
if (isCycleComplete && nextMedia_.isValid()) { if (isCycleComplete && nextMedia_.isValid()) {
lastAnimationState_ = animationStrategy_->getState();
predictedNextAnimationMode_ = predictNextAnimationMode();
currentState_ = RenderState::TRANSITIONING; currentState_ = RenderState::TRANSITIONING;
transitionStartTime_ = std::chrono::steady_clock::now(); transitionStartTime_ = std::chrono::steady_clock::now();
// 사용자가 설정한 전환 모드를 확인
TransitionMode transModeToUse = configuredTransitionMode_; TransitionMode transModeToUse = configuredTransitionMode_;
if (transModeToUse == TransitionMode::RANDOM) { if (transModeToUse == TransitionMode::RANDOM) {
// RANDOM일 경우, FADE, SLIDE, MOSAIC 중에서 무작위로 선택
std::uniform_int_distribution<int> dist(0, 2); std::uniform_int_distribution<int> dist(0, 2);
int randomChoice = dist(randomEngine_); transModeToUse = static_cast<TransitionMode>(dist(randomEngine_));
if (randomChoice == 0) transModeToUse = TransitionMode::FADE;
else if (randomChoice == 1) transModeToUse = TransitionMode::SLIDE;
else transModeToUse = TransitionMode::MOSAIC;
} }
// 결정된 모드에 맞는 '전환 전문가' 객체를 생성
if (transModeToUse == TransitionMode::SLIDE) { if (transModeToUse == TransitionMode::SLIDE) {
transitionStrategy_ = std::make_unique<SlideTransition>(fadeDurationMs_, surfaceWidth); transitionStrategy_ = std::make_unique<SlideTransition>(fadeDurationMs_, surfaceWidth);
} else if (transModeToUse == TransitionMode::MOSAIC) { } else if (transModeToUse == TransitionMode::MOSAIC) {
transitionStrategy_ = std::make_unique<MosaicTransition>(fadeDurationMs_, 20, 32, randomEngine_); transitionStrategy_ = std::make_unique<MosaicTransition>(fadeDurationMs_, 20, 32, randomEngine_);
} else { // 기본값은 FADE } else {
transitionStrategy_ = std::make_unique<FadeTransition>(fadeDurationMs_); transitionStrategy_ = std::make_unique<FadeTransition>(fadeDurationMs_);
} }
// 새로운 전환 전문가의 상태를 초기화
if(transitionStrategy_) transitionStrategy_->reset(); if(transitionStrategy_) transitionStrategy_->reset();
LOGI("Animation complete. Switching to TRANSITIONING state.");
} }
} }
/**
* @brief .
*/
Renderer::AnimationMode Renderer::predictNextAnimationMode() {
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);
return availableModes[dist(randomEngine_)];
} else {
return configuredAnimationMode_;
}
}
/**
* @brief (, ) .
*/
AnimationState Renderer::getStartStateForMode(AnimationMode mode, int surfaceWidth, int surfaceHeight) {
AnimationState startState;
if (mode == AnimationMode::PAN || mode == AnimationMode::PAN_ONE_WAY) {
// Pan 계열은 가장자리(0,0)에서 시작
startState.offsetX = 0.0f;
startState.offsetY = 0.0f;
// Pan 계열의 기본 스케일 계산
float scale;
if ((static_cast<float>(nextMedia_.getWidth()) / nextMedia_.getHeight()) > ((float)surfaceWidth / surfaceHeight)) {
scale = (float)surfaceHeight / nextMedia_.getHeight();
} else {
scale = (float)surfaceWidth / nextMedia_.getWidth();
}
startState.scale = scale;
} else {
// Zoom, None, PageTurn 등은 중앙 정렬에서 시작
float baseScale, baseOffsetX, baseOffsetY;
calculateFitScaleAndOffset(nextMedia_, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY);
startState.offsetX = baseOffsetX;
startState.offsetY = baseOffsetY;
startState.scale = baseScale;
}
return startState;
}
void Renderer::handleTransitionState(ANativeWindow_Buffer& buffer, int surfaceWidth, int surfaceHeight) { void Renderer::handleTransitionState(ANativeWindow_Buffer& buffer, int surfaceWidth, int surfaceHeight) {
if (!transitionStrategy_ || !nextMedia_.isValid()) { if (!transitionStrategy_ || !nextMedia_.isValid()) {
currentState_ = RenderState::ANIMATING; currentState_ = RenderState::ANIMATING;
if(animationStrategy_) animationStrategy_->reset(); if(animationStrategy_) animationStrategy_->reset();
return; return;
} }
// 2. 해당 모드의 전문가를 "임시로" 고용하여 시작 상태가 어떨지 물어봅니다.
std::unique_ptr<AnimationStrategy> tempNextStrategy;
switch (predictedNextAnimationMode_) {
case AnimationMode::PAN:
tempNextStrategy = std::make_unique<PanAnimation>(animationSpeed_);
break;
case AnimationMode::PAN_ONE_WAY:
tempNextStrategy = std::make_unique<PanOneWayAnimation>(animationSpeed_);
break;
case AnimationMode::ZOOM:
tempNextStrategy = std::make_unique<ZoomAnimation>(animationSpeed_);
break;
case AnimationMode::PAGE_TURN:
tempNextStrategy = std::make_unique<PageTurnAnimation>(animationSpeed_, pageTurnDelayMs_);
break;
case AnimationMode::NONE:
default:
tempNextStrategy = std::make_unique<NoneAnimation>(animationSpeed_);
break;
}
AnimationState destStartState;
if (tempNextStrategy) {
destStartState = tempNextStrategy->getStartState(this, nextMedia_, surfaceWidth, surfaceHeight);
}
// 3. 전환 전문가에게 모든 정보를 (이전 상태의 끝, 다음 상태의 시작) 넘겨주고 그리기를 위임
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
long long elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - transitionStartTime_).count(); long long elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - transitionStartTime_).count();
bool isComplete = transitionStrategy_->isComplete(elapsed); bool isComplete = transitionStrategy_->execute(this, buffer, currentMedia_, lastAnimationState_, nextMedia_, destStartState, elapsed);
transitionStrategy_->execute(this, buffer, currentMedia_, nextMedia_, elapsed);
// 4. 상태 전환 체크
if (isComplete) { if (isComplete) {
currentMedia_ = std::move(nextMedia_); currentMedia_ = std::move(nextMedia_);
currentState_ = RenderState::ANIMATING; currentState_ = RenderState::ANIMATING;
animationCycleComplete_ = false; // 새 미디어의 애니메이션 시작 준비 // 예측했던 모드를 실제로 적용
determineActiveAnimationMode(); activeAnimationMode_ = predictedNextAnimationMode_;
switch (activeAnimationMode_) { // 해당 전략 객체 생성
case AnimationMode::PAN: animationStrategy_ = std::make_unique<PanAnimation>(animationSpeed_); break;
case AnimationMode::PAN_ONE_WAY: animationStrategy_ = std::make_unique<PanOneWayAnimation>(animationSpeed_); break;
case AnimationMode::ZOOM: animationStrategy_ = std::make_unique<ZoomAnimation>(animationSpeed_); break;
case AnimationMode::PAGE_TURN: animationStrategy_ = std::make_unique<PageTurnAnimation>(animationSpeed_, pageTurnDelayMs_); break;
case AnimationMode::NONE:
default: animationStrategy_ = std::make_unique<NoneAnimation>(animationSpeed_);
break;
}
if(animationStrategy_) animationStrategy_->reset(); if(animationStrategy_) animationStrategy_->reset();
} }
} }
@ -218,7 +241,6 @@ void Renderer::renderFrame(ANativeWindow* window) {
// ==================================================================== // ====================================================================
// 하위 그리기 함수 및 헬퍼 함수들 // 하위 그리기 함수 및 헬퍼 함수들
// ==================================================================== // ====================================================================
void Renderer::drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float alpha, float finalOffsetX, float finalOffsetY, float finalScale) { void Renderer::drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float alpha, float finalOffsetX, float finalOffsetY, float finalScale) {
if (!media.isValid() || alpha <= 0.0f) return; if (!media.isValid() || alpha <= 0.0f) return;
if (media.getType() == MediaAsset::Type::IMAGE) { if (media.getType() == MediaAsset::Type::IMAGE) {
@ -239,11 +261,11 @@ void Renderer::renderImageFrame(const MediaAsset& media, ANativeWindow_Buffer& b
uint8_t alphaByte = static_cast<uint8_t>(alpha * 255.0f); uint8_t alphaByte = static_cast<uint8_t>(alpha * 255.0f);
for (int y = 0; y < buffer.height; ++y) { 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; if (srcY < 0 || srcY >= imgH) continue;
uint32_t* dstRow = dstPixels + y * dstStride; uint32_t* dstRow = dstPixels + y * dstStride;
for (int x = 0; x < buffer.width; ++x) { 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; if (srcX < 0 || srcX >= imgW) continue;
const uint8_t* srcPixel = &pixelData[(srcY * imgW + srcX) * 4]; const uint8_t* srcPixel = &pixelData[(srcY * imgW + srcX) * 4];
uint32_t dstPixelValue = dstRow[x]; uint32_t dstPixelValue = dstRow[x];

View File

@ -9,47 +9,34 @@
#include <chrono> #include <chrono>
#include <mutex> #include <mutex>
#include <random> #include <random>
#include <memory> // for std::unique_ptr #include <memory>
#include <android/native_window.h> #include <android/native_window.h>
#include <android/native_window_jni.h> #include <android/native_window_jni.h>
class Renderer { class Renderer {
public: public:
// 사용자가 설정할 수 있는 애니메이션 모드 종류
enum class AnimationMode { enum class AnimationMode {
PAN = 0, PAN = 0, ZOOM = 1, NONE = 2, RANDOM = 3,
ZOOM = 1, PAN_ONE_WAY = 4, PAGE_TURN = 5
NONE = 2,
RANDOM = 3,
PAN_ONE_WAY = 4,
PAGE_TURN = 5
}; };
// 사용자가 설정할 수 있는 전환 효과 종류
enum class TransitionMode { enum class TransitionMode {
FADE = 0, FADE = 0, SLIDE = 1, RANDOM = 2, MOSAIC = 3
SLIDE = 1,
RANDOM = 2,
MOSAIC = 3
}; };
Renderer(); Renderer();
~Renderer(); ~Renderer();
// --- Public API ---
void setNextMedia(int fd); void setNextMedia(int fd);
void renderFrame(ANativeWindow* window); void renderFrame(ANativeWindow* window);
void release(); void release();
std::string getDebugInfo() const; std::string getDebugInfo() const;
// --- 설정(Settings)을 위한 함수들 ---
void setAnimationSpeed(float speed); void setAnimationSpeed(float speed);
void setAnimationMode(int mode); void setAnimationMode(int mode);
void setFadeDuration(int durationMs); void setFadeDuration(int durationMs);
void setPageTurnDelay(int delayMs); void setPageTurnDelay(int delayMs);
void setTransitionMode(int mode); void setTransitionMode(int mode);
// --- 전략(Strategy) 객체들이 호출하는 헬퍼 함수들 ---
void drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float alpha, float finalOffsetX, float finalOffsetY, float finalScale); void drawMedia(ANativeWindow_Buffer& buffer, MediaAsset& media, float alpha, float finalOffsetX, float finalOffsetY, float finalScale);
void renderVideoFrame(MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha); 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); void renderImageFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha);
@ -57,26 +44,22 @@ public:
float& outScale, float& outOffsetX, float& outOffsetY) const; float& outScale, float& outOffsetX, float& outOffsetY) const;
private: private:
// --- 상태 머신(State Machine) --- enum class RenderState { ANIMATING, TRANSITIONING };
enum class RenderState {
ANIMATING,
TRANSITIONING
};
RenderState currentState_ = RenderState::ANIMATING; RenderState currentState_ = RenderState::ANIMATING;
bool animationCycleComplete_ = false; // <-- 이 플래그가 다시 필요
// --- 상태별 처리를 위한 private 핸들러 함수 ---
void handleAnimationState(ANativeWindow_Buffer& buffer, int surfaceWidth, int surfaceHeight); void handleAnimationState(ANativeWindow_Buffer& buffer, int surfaceWidth, int surfaceHeight);
void handleTransitionState(ANativeWindow_Buffer& buffer, int surfaceWidth, int surfaceHeight); void handleTransitionState(ANativeWindow_Buffer& buffer, int surfaceWidth, int surfaceHeight);
// --- 멤버 객체들 ---
std::mutex renderMutex_; std::mutex renderMutex_;
Preloader preloader_; Preloader preloader_;
MediaAsset currentMedia_; MediaAsset currentMedia_;
MediaAsset nextMedia_; MediaAsset nextMedia_;
std::unique_ptr<AnimationStrategy> animationStrategy_;
std::unique_ptr<TransitionStrategy> transitionStrategy_;
// --- 설정값 저장 변수 --- std::unique_ptr<AnimationStrategy> animationStrategy_;
AnimationMode predictedNextAnimationMode_;
std::unique_ptr<TransitionStrategy> transitionStrategy_;
AnimationState lastAnimationState_;
AnimationMode configuredAnimationMode_ = AnimationMode::PAN; AnimationMode configuredAnimationMode_ = AnimationMode::PAN;
TransitionMode configuredTransitionMode_ = TransitionMode::FADE; TransitionMode configuredTransitionMode_ = TransitionMode::FADE;
AnimationMode activeAnimationMode_ = AnimationMode::PAN; AnimationMode activeAnimationMode_ = AnimationMode::PAN;
@ -84,12 +67,11 @@ private:
long long pageTurnDelayMs_ = 5000; long long pageTurnDelayMs_ = 5000;
float animationSpeed_ = 1.0f; float animationSpeed_ = 1.0f;
// --- 상태(State) 관리 변수 --- AnimationState getStartStateForMode(AnimationMode mode, int surfaceWidth, int surfaceHeight);
std::chrono::steady_clock::time_point transitionStartTime_; AnimationMode predictNextAnimationMode();
// --- 랜덤 기능 --- std::chrono::steady_clock::time_point transitionStartTime_;
std::mt19937 randomEngine_; std::mt19937 randomEngine_;
// --- private 헬퍼 함수 ---
void determineActiveAnimationMode(); void determineActiveAnimationMode();
}; };

View File

@ -1,74 +1,68 @@
#include "TransitionStrategy.h" #include "TransitionStrategy.h"
#include "Renderer.h" #include "Renderer.h"
#include <algorithm> // for std::clamp #include <algorithm>
#include <vector> #include <vector>
#include <numeric> // for std::iota #include <numeric>
#include <random> // for std::shuffle #include <random>
// ====================================================================
// --- 페이드 전환 효과 --- // --- 페이드 전환 효과 ---
// ====================================================================
class FadeTransition : public TransitionStrategy { class FadeTransition : public TransitionStrategy {
public: public:
FadeTransition(long long duration) : TransitionStrategy(duration) {} FadeTransition(long long duration) : TransitionStrategy(duration) {}
void reset() override {} void reset() override {}
bool isComplete(long long elapsedMs) const override { return elapsedMs >= durationMs_; }
bool isComplete(long long elapsedMs) const override { bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer,
return elapsedMs >= durationMs_; MediaAsset& source, const AnimationState& sourceEnd,
} MediaAsset& dest, const AnimationState& destStart,
long long elapsedMs) override {
void execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& source, MediaAsset& dest, long long elapsedMs) override {
float progress = std::clamp((float)elapsedMs / durationMs_, 0.0f, 1.0f); float progress = std::clamp((float)elapsedMs / durationMs_, 0.0f, 1.0f);
// 1. 사라지는 이미지(source)의 올바른 위치와 크기를 계산 // source는 전달받은 마지막 상태(sourceEnd) 그대로 페이드 아웃
float srcScale, srcOffsetX, srcOffsetY; renderer->drawMedia(buffer, source, 1.0f - progress, sourceEnd.offsetX, sourceEnd.offsetY, sourceEnd.scale);
renderer->calculateFitScaleAndOffset(source, buffer.width, buffer.height, srcScale, srcOffsetX, srcOffsetY); // dest는 전달받은 시작 상태(destStart) 그대로 페이드 인
renderer->drawMedia(buffer, dest, progress, destStart.offsetX, destStart.offsetY, destStart.scale);
// 2. 나타나는 이미지(dest)의 올바른 위치와 크기를 계산
float destScale, destOffsetX, destOffsetY;
renderer->calculateFitScaleAndOffset(dest, buffer.width, buffer.height, destScale, destOffsetX, destOffsetY);
// 3. 계산된 값으로 두 이미지를 그림
renderer->drawMedia(buffer, source, 1.0f - progress, srcOffsetX, srcOffsetY, srcScale);
renderer->drawMedia(buffer, dest, progress, destOffsetX, destOffsetY, destScale);
return isComplete(elapsedMs);
} }
}; };
// --- 슬라이드 전환 효과 --- // --- ⬇️ 슬라이드 전환 효과 (수정) ⬇️ ---
class SlideTransition : public TransitionStrategy { class SlideTransition : public TransitionStrategy {
public: public:
SlideTransition(long long duration, int surfaceWidth) SlideTransition(long long duration, int surfaceWidth)
: TransitionStrategy(duration), surfaceWidth_(surfaceWidth) {} : TransitionStrategy(duration), surfaceWidth_(surfaceWidth) {}
void reset() override {} void reset() override {}
bool isComplete(long long elapsedMs) const override { return elapsedMs >= durationMs_; }
bool isComplete(long long elapsedMs) const override { bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer,
return elapsedMs >= durationMs_; MediaAsset& source, const AnimationState& sourceEnd,
} MediaAsset& dest, const AnimationState& destStart,
long long elapsedMs) override {
void execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& source, MediaAsset& dest, long long elapsedMs) override {
float progress = std::clamp((float)elapsedMs / durationMs_, 0.0f, 1.0f); float progress = std::clamp((float)elapsedMs / durationMs_, 0.0f, 1.0f);
float easeProgress = progress * progress * (3.0f - 2.0f * progress); float easeProgress = progress * progress * (3.0f - 2.0f * progress);
// 1. 두 이미지의 기본 중앙 위치를 먼저 계산
float srcScale, srcBaseOffsetX, srcBaseOffsetY;
renderer->calculateFitScaleAndOffset(source, buffer.width, buffer.height, srcScale, srcBaseOffsetX, srcBaseOffsetY);
float destScale, destBaseOffsetX, destBaseOffsetY;
renderer->calculateFitScaleAndOffset(dest, buffer.width, buffer.height, destScale, destBaseOffsetX, destBaseOffsetY);
// 2. 슬라이드 효과를 위한 X축 이동거리 계산
float sourceSlideX = -surfaceWidth_ * easeProgress; float sourceSlideX = -surfaceWidth_ * easeProgress;
float destSlideX = surfaceWidth_ * (1.0f - easeProgress); float destSlideX = surfaceWidth_ * (1.0f - easeProgress);
// 3. 기본 위치에 슬라이드 이동거리를 더해서 최종 위치 결정 // source는 마지막 위치(sourceEnd)에서 왼쪽으로 슬라이드 아웃
renderer->drawMedia(buffer, source, 1.0f, srcBaseOffsetX + sourceSlideX, srcBaseOffsetY, srcScale); renderer->drawMedia(buffer, source, 1.0f, sourceEnd.offsetX + sourceSlideX, sourceEnd.offsetY, sourceEnd.scale);
renderer->drawMedia(buffer, dest, 1.0f, destBaseOffsetX + destSlideX, destBaseOffsetY, destScale); // dest는 자신의 시작 위치(destStart) 오른쪽 밖에서 중앙으로 슬라이드 인
renderer->drawMedia(buffer, dest, 1.0f, destStart.offsetX + destSlideX, destStart.offsetY, destStart.scale);
return isComplete(elapsedMs);
} }
private: private:
int surfaceWidth_; int surfaceWidth_;
}; };
// ====================================================================
// --- ⬇️ 모자이크 전환 효과 (전체 구현) ⬇️ --- // --- 모자이크 전환 효과 ---
// ====================================================================
class MosaicTransition : public TransitionStrategy { class MosaicTransition : public TransitionStrategy {
public: public:
MosaicTransition(long long duration, int tilesX, int tilesY, std::mt19937& randomEngine) MosaicTransition(long long duration, int tilesX, int tilesY, std::mt19937& randomEngine)
@ -76,14 +70,11 @@ public:
reset(); reset();
} }
// 전환 시작 시 타일 순서를 섞음
void reset() override { void reset() override {
int totalTiles = tilesX_ * tilesY_; int totalTiles = tilesX_ * tilesY_;
tileOrder_.resize(totalTiles); tileOrder_.resize(totalTiles);
std::iota(tileOrder_.begin(), tileOrder_.end(), 0); // 0, 1, 2, ... 순서로 벡터 채우기 std::iota(tileOrder_.begin(), tileOrder_.end(), 0);
std::shuffle(tileOrder_.begin(), tileOrder_.end(), randomEngine_); // 순서 섞기 std::shuffle(tileOrder_.begin(), tileOrder_.end(), randomEngine_);
// 빠른 조회를 위한 역순 조회 테이블 생성
revealOrder_.resize(totalTiles); revealOrder_.resize(totalTiles);
for(size_t i = 0; i < tileOrder_.size(); ++i) { for(size_t i = 0; i < tileOrder_.size(); ++i) {
revealOrder_[tileOrder_[i]] = i; revealOrder_[tileOrder_[i]] = i;
@ -94,67 +85,57 @@ public:
return elapsedMs >= durationMs_; return elapsedMs >= durationMs_;
} }
// 모자이크 효과를 픽셀 단위로 직접 그림 bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer,
void execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& source, MediaAsset& dest, long long elapsedMs) override { MediaAsset& source, const AnimationState& sourceEnd,
MediaAsset& dest, const AnimationState& destStart,
long long elapsedMs) override {
float progress = std::clamp((float)elapsedMs / durationMs_, 0.0f, 1.0f); float progress = std::clamp((float)elapsedMs / durationMs_, 0.0f, 1.0f);
int totalTiles = tilesX_ * tilesY_; int totalTiles = tilesX_ * tilesY_;
int tilesToReveal = static_cast<int>(progress * totalTiles); int tilesToReveal = static_cast<int>(progress * totalTiles);
// 두 이미지의 기본 스케일과 오프셋을 미리 계산
float srcScale, srcOffsetX, srcOffsetY;
renderer->calculateFitScaleAndOffset(source, buffer.width, buffer.height, srcScale, srcOffsetX, srcOffsetY);
float destScale, destOffsetX, destOffsetY;
renderer->calculateFitScaleAndOffset(dest, buffer.width, buffer.height, destScale, destOffsetX, destOffsetY);
const uint8_t* srcPixelData = source.getType() == MediaAsset::Type::IMAGE ? source.getImageData() : source.getRgbBuffer().data(); const uint8_t* srcPixelData = source.getType() == MediaAsset::Type::IMAGE ? source.getImageData() : source.getRgbBuffer().data();
const uint8_t* destPixelData = dest.getType() == MediaAsset::Type::IMAGE ? dest.getImageData() : dest.getRgbBuffer().data(); const uint8_t* destPixelData = dest.getType() == MediaAsset::Type::IMAGE ? dest.getImageData() : dest.getRgbBuffer().data();
if (!srcPixelData || !destPixelData) return isComplete(elapsedMs);
if (!srcPixelData || !destPixelData) return;
uint32_t* dstPixels = (uint32_t*)buffer.bits; uint32_t* dstPixels = (uint32_t*)buffer.bits;
int dstStride = buffer.stride; int dstStride = buffer.stride;
float tileWidth = (float)buffer.width / tilesX_; float tileWidth = (float)buffer.width / tilesX_;
float tileHeight = (float)buffer.height / tilesY_; float tileHeight = (float)buffer.height / tilesY_;
// 화면의 모든 픽셀을 순회
for (int y = 0; y < buffer.height; ++y) { for (int y = 0; y < buffer.height; ++y) {
uint32_t* dstRow = dstPixels + y * dstStride; uint32_t* dstRow = dstPixels + y * dstStride;
for (int x = 0; x < buffer.width; ++x) { for (int x = 0; x < buffer.width; ++x) {
// 현재 픽셀이 속한 타일의 인덱스 계산
int tileX = static_cast<int>(x / tileWidth); int tileX = static_cast<int>(x / tileWidth);
int tileY = static_cast<int>(y / tileHeight); int tileY = static_cast<int>(y / tileHeight);
int tileIndex = tileY * tilesX_ + tileX; int tileIndex = tileY * tilesX_ + tileX;
// 이 타일이 몇 번째로 드러나야 하는지(rank) 확인
int rank = revealOrder_[tileIndex]; int rank = revealOrder_[tileIndex];
const uint8_t* finalPixelData; const uint8_t* finalPixelData;
if (rank < tilesToReveal) { if (rank < tilesToReveal) {
// 드러나야 할 타일이면, 새 이미지(dest)에서 픽셀을 가져옴 // --- ⬇️ 수정된 부분: (x - offsetX) 공식 적용 ⬇️ ---
int srcX = static_cast<int>((x + destOffsetX) / destScale); int srcX = static_cast<int>((x - destStart.offsetX) / destStart.scale);
int srcY = static_cast<int>((y + destOffsetY) / destScale); int srcY = static_cast<int>((y - destStart.offsetY) / destStart.scale);
if (srcX >= 0 && srcX < dest.getWidth() && srcY >= 0 && srcY < dest.getHeight()) { if (srcX >= 0 && srcX < dest.getWidth() && srcY >= 0 && srcY < dest.getHeight()) {
finalPixelData = &destPixelData[(srcY * dest.getWidth() + srcX) * 4]; finalPixelData = &destPixelData[(srcY * dest.getWidth() + srcX) * 4];
} else { continue; } } else { continue; }
} else { } else {
// 아직 드러나지 않은 타일이면, 이전 이미지(source)에서 픽셀을 가져옴 // --- ⬇️ 수정된 부분: (x - offsetX) 공식 적용 ⬇️ ---
int srcX = static_cast<int>((x + srcOffsetX) / srcScale); int srcX = static_cast<int>((x - sourceEnd.offsetX) / sourceEnd.scale);
int srcY = static_cast<int>((y + srcOffsetY) / srcScale); int srcY = static_cast<int>((y - sourceEnd.offsetY) / sourceEnd.scale);
if (srcX >= 0 && srcX < source.getWidth() && srcY >= 0 && srcY < source.getHeight()) { if (srcX >= 0 && srcX < source.getWidth() && srcY >= 0 && srcY < source.getHeight()) {
finalPixelData = &srcPixelData[(srcY * source.getWidth() + srcX) * 4]; finalPixelData = &srcPixelData[(srcY * source.getWidth() + srcX) * 4];
} else { continue; } } else { continue; }
} }
// 최종 픽셀을 버퍼에 씀
dstRow[x] = (0xFF << 24) | (finalPixelData[0] << 16) | (finalPixelData[1] << 8) | finalPixelData[2]; dstRow[x] = (0xFF << 24) | (finalPixelData[0] << 16) | (finalPixelData[1] << 8) | finalPixelData[2];
} }
} }
return isComplete(elapsedMs);
} }
private: private:
int tilesX_, tilesY_; int tilesX_, tilesY_;
std::vector<int> tileOrder_; std::vector<int> tileOrder_;
std::vector<int> revealOrder_; // 역순 조회 테이블 std::vector<int> revealOrder_;
std::mt19937& randomEngine_; std::mt19937& randomEngine_;
}; };

View File

@ -1,10 +1,13 @@
#pragma once #pragma once
#include <android/native_window.h> #include <android/native_window.h>
#include "MediaAsset.h" #include "MediaAsset.h"
#include "AnimationStrategy.h" // AnimationState를 사용하기 위해 포함
// Renderer 클래스에 대한 전방 선언 (순환 참조 방지) class Renderer; // 전방 선언
class Renderer;
/**
* @brief (Strategy) .
*/
class TransitionStrategy { class TransitionStrategy {
public: public:
virtual ~TransitionStrategy() = default; virtual ~TransitionStrategy() = default;
@ -19,14 +22,14 @@ public:
/** /**
* @brief . * @brief .
* @param renderer Renderer ( ) * @param sourceEnd
* @param buffer * @param destStart
* @param source * @return true를 .
* @param dest
* @param elapsedMs
* @return true, false
*/ */
virtual void execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& source, MediaAsset& dest, long long elapsedMs) = 0; virtual bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer,
MediaAsset& source, const AnimationState& sourceEnd,
MediaAsset& dest, const AnimationState& destStart,
long long elapsedMs) = 0;
virtual void reset() = 0; virtual void reset() = 0;

View File

@ -29,7 +29,7 @@ class MyWallpaperService : WallpaperService() {
private var isVisible = false private var isVisible = false
private var isSurfaceValid = false private var isSurfaceValid = false
private val frameDelayMs = 1000L / 45L private val frameDelayMs = 1000L / 50L
private val syncDelayMs = 250L // 상태 변경을 처리하기 전의 지연 시간 private val syncDelayMs = 250L // 상태 변경을 처리하기 전의 지연 시간
private var frameCount = 0 private var frameCount = 0
private val debugLogCheck = 240 private val debugLogCheck = 240
@ -183,11 +183,9 @@ class MyWallpaperService : WallpaperService() {
nativeRenderer?.initialize() // nativeInit() -> initialize() nativeRenderer?.initialize() // nativeInit() -> initialize()
nativeRenderer?.setFadeDuration(1500) nativeRenderer?.setFadeDuration(1500)
nativeRenderer?.setTurnPageDuration(8000) nativeRenderer?.setTurnPageDuration(8000)
nativeRenderer?.setAnimationMode(NativeRenderer.ANIMATION_MODE_PAN) nativeRenderer?.setAnimationMode(NativeRenderer.ANIMATION_MODE_RANDOM)
nativeRenderer?.setTransitionMode(NativeRenderer.TRANSITION_MODE_FADE) nativeRenderer?.setTransitionMode(NativeRenderer.TRANSITION_MODE_RANDOM)
nativeRenderer?.setAnimationSpeed(10.0f) // nativeSetAnimationSpeed -> setAnimationSpeed nativeRenderer?.setAnimationSpeed(2.0f)
NativeRenderer.nativeSetNextMediaCallback(nextMediaCallback) NativeRenderer.nativeSetNextMediaCallback(nextMediaCallback)
@ -197,10 +195,7 @@ class MyWallpaperService : WallpaperService() {
} }
// ... loadMediaFiles, nextMediaCallback, getFdFromPath는 이전과 동일 ... // ... loadMediaFiles, nextMediaCallback, getFdFromPath는 이전과 동일 ...
private fun loadMediaFiles() { private fun loadMediaFiles() {
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallPapers")
if (!mediaDir.exists()) mediaDir.mkdirs() if (!mediaDir.exists()) mediaDir.mkdirs()
val supportedExtensions = listOf("mp4", "mkv", "avi", "mov", "webm", "jpg", "jpeg", "png", "bmp", "webp")
mediaFiles = mediaDir.listFiles() mediaFiles = mediaDir.listFiles()
?.filter { supportedExtensions.contains(it.extension.lowercase()) } ?.filter { supportedExtensions.contains(it.extension.lowercase()) }
?.toList() ?: emptyList() ?.toList() ?: emptyList()
@ -217,13 +212,15 @@ class MyWallpaperService : WallpaperService() {
} }
} }
} }
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallPapers")
val supportedExtensions = listOf("mp4", "mkv", "avi", "mov","webm", "jpg", "jpeg", "png", "bmp", "webp", "gif")
private val nextMediaCallback = object : NativeRenderer.NextMediaCallback { private val nextMediaCallback = object : NativeRenderer.NextMediaCallback {
override fun onNextMediaRequested() { override fun onNextMediaRequested() {
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallPapers")
if (!mediaDir.exists()) mediaDir.mkdirs() if (!mediaDir.exists()) mediaDir.mkdirs()
val supportedExtensions = listOf("mp4", "mkv", "avi", "mov", "webm", "jpg", "jpeg", "png", "bmp", "webp")
mediaFiles = mediaDir.listFiles() mediaFiles = mediaDir.listFiles()
?.filter { supportedExtensions.contains(it.extension.lowercase()) } ?.filter { supportedExtensions.contains(it.extension.lowercase()) }
?.toList() ?: emptyList() ?.toList() ?: emptyList()