ㅇㅇ
This commit is contained in:
parent
167c433a59
commit
03086981e5
@ -1,6 +1,6 @@
|
||||
#include "AnimationStrategy.h"
|
||||
#include "Renderer.h" // Renderer의 그리기 헬퍼 함수들을 사용하기 위해 포함
|
||||
#include <algorithm> // for std::max, std::clamp
|
||||
#include "Renderer.h"
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <numeric>
|
||||
#include <random>
|
||||
@ -13,24 +13,27 @@ public:
|
||||
PanAnimation(float speed) : AnimationStrategy(speed) { reset(); }
|
||||
|
||||
void reset() override {
|
||||
offsetX_ = 0.0f;
|
||||
offsetY_ = 0.0f;
|
||||
cycleComplete_ = false;
|
||||
xDirection_ = 1;
|
||||
yDirection_ = 1;
|
||||
state_.offsetX = 0.0f; state_.offsetY = 0.0f;
|
||||
state_.scale = 1.0f; cycleComplete_ = false;
|
||||
xDirection_ = -1; // 시작 방향을 -1 (좌로 이동)으로 변경
|
||||
yDirection_ = -1; // 시작 방향을 -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;
|
||||
}
|
||||
|
||||
bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
|
||||
// 1. 애니메이션이 이미 끝났으면 더 이상 계산하지 않음
|
||||
if (cycleComplete_) {
|
||||
// 마지막 위치에 고정하여 그림
|
||||
renderer->drawMedia(buffer, media, 1.0f, offsetX_, offsetY_, getBaseScale(media, surfaceWidth, surfaceHeight));
|
||||
return cycleComplete_;
|
||||
}
|
||||
AnimationState getState() const override { return state_; }
|
||||
|
||||
bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) override {
|
||||
bool cycleComplete = false;
|
||||
|
||||
// 2. Pan 모드에 필요한 overflow(화면 밖으로 넘친 영역) 계산
|
||||
float overflowX = 0.0f, overflowY = 0.0f;
|
||||
float scale = getBaseScale(media, surfaceWidth, surfaceHeight);
|
||||
state_.scale = scale; // 마지막 상태 저장을 위해 현재 스케일값 기록
|
||||
float mediaW = static_cast<float>(media.getWidth());
|
||||
float mediaH = static_cast<float>(media.getHeight());
|
||||
if ((mediaW / mediaH) > ((float)surfaceWidth / surfaceHeight)) {
|
||||
@ -39,38 +42,35 @@ public:
|
||||
overflowY = std::max(0.0f, mediaH * scale - surfaceHeight);
|
||||
}
|
||||
|
||||
// 3. 좌표 업데이트
|
||||
bool xDone = (overflowX <= 0);
|
||||
bool yDone = (overflowY <= 0);
|
||||
|
||||
if (overflowX > 0) {
|
||||
offsetX_ += animationSpeed_ * xDirection_;
|
||||
if (xDirection_ == 1 && offsetX_ >= overflowX) { offsetX_ = overflowX; xDirection_ = -1; }
|
||||
else if (xDirection_ == -1 && offsetX_ <= 0) { offsetX_ = 0; xDirection_ = 1; xDone = true; }
|
||||
}
|
||||
if (overflowY > 0) {
|
||||
offsetY_ += animationSpeed_ * yDirection_;
|
||||
if (yDirection_ == 1 && offsetY_ >= overflowY) { offsetY_ = overflowY; yDirection_ = -1; }
|
||||
else if (yDirection_ == -1 && offsetY_ <= 0) { offsetY_ = 0; yDirection_ = 1; yDone = true; }
|
||||
if (!cycleComplete_) {
|
||||
bool xDone = (overflowX <= 0);
|
||||
bool yDone = (overflowY <= 0);
|
||||
if (overflowX > 0) {
|
||||
// offsetX를 음수 방향으로 움직임 (0 -> -overflowX)
|
||||
state_.offsetX += animationSpeed_ * xDirection_;
|
||||
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) {
|
||||
// offsetY를 음수 방향으로 움직임 (0 -> -overflowY)
|
||||
state_.offsetY += animationSpeed_ * yDirection_;
|
||||
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축 왕복이 모두 끝났는지 확인
|
||||
if (xDone && yDone) {
|
||||
cycleComplete_ = true;
|
||||
}
|
||||
|
||||
// 5. 계산된 최종 좌표로 그림
|
||||
renderer->drawMedia(buffer, media, 1.0f, offsetX_, offsetY_, scale);
|
||||
|
||||
renderer->drawMedia(buffer, media, 1.0f, state_.offsetX, state_.offsetY, state_.scale);
|
||||
return cycleComplete_;
|
||||
}
|
||||
|
||||
private:
|
||||
float offsetX_, offsetY_;
|
||||
AnimationState state_;
|
||||
int xDirection_, yDirection_;
|
||||
bool cycleComplete_;
|
||||
|
||||
float getBaseScale(MediaAsset& media, int surfaceWidth, int surfaceHeight) {
|
||||
if (media.getHeight() == 0 || surfaceHeight == 0) return 1.0f;
|
||||
float scale;
|
||||
if ((static_cast<float>(media.getWidth()) / media.getHeight()) > ((float)surfaceWidth / surfaceHeight)) {
|
||||
scale = (float)surfaceHeight / media.getHeight();
|
||||
@ -88,21 +88,21 @@ private:
|
||||
class PanOneWayAnimation : public AnimationStrategy {
|
||||
public:
|
||||
PanOneWayAnimation(float speed) : AnimationStrategy(speed) { reset(); }
|
||||
void reset() override { state_ = AnimationState(); cycleComplete_ = false; }
|
||||
AnimationState getState() const override { return state_; }
|
||||
|
||||
void reset() override {
|
||||
offsetX_ = 0.0f;
|
||||
offsetY_ = 0.0f;
|
||||
cycleComplete_ = false;
|
||||
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;
|
||||
}
|
||||
|
||||
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 scale = getBaseScale(media, surfaceWidth, surfaceHeight);
|
||||
state_.scale = scale;
|
||||
float mediaW = static_cast<float>(media.getWidth());
|
||||
float mediaH = static_cast<float>(media.getHeight());
|
||||
if ((mediaW / mediaH) > ((float)surfaceWidth / surfaceHeight)) {
|
||||
@ -110,33 +110,30 @@ public:
|
||||
} else {
|
||||
overflowY = std::max(0.0f, mediaH * scale - surfaceHeight);
|
||||
}
|
||||
|
||||
bool xReachedEnd = (overflowX <= 0);
|
||||
bool yReachedEnd = (overflowY <= 0);
|
||||
|
||||
if (overflowX > 0) {
|
||||
offsetX_ += animationSpeed_;
|
||||
if (offsetX_ >= overflowX) { offsetX_ = overflowX; xReachedEnd = true; }
|
||||
}
|
||||
if (overflowY > 0) {
|
||||
offsetY_ += animationSpeed_;
|
||||
if (offsetY_ >= overflowY) { offsetY_ = overflowY; yReachedEnd = true; }
|
||||
if (!cycleComplete_) {
|
||||
bool xReachedEnd = (overflowX <= 0);
|
||||
bool yReachedEnd = (overflowY <= 0);
|
||||
if (overflowX > 0) {
|
||||
// offsetX를 음수 방향으로만 이동
|
||||
state_.offsetX -= animationSpeed_;
|
||||
if (state_.offsetX <= -overflowX) { state_.offsetX = -overflowX; xReachedEnd = true; }
|
||||
}
|
||||
if (overflowY > 0) {
|
||||
// offsetY를 음수 방향으로만 이동
|
||||
state_.offsetY -= animationSpeed_;
|
||||
if (state_.offsetY <= -overflowY) { state_.offsetY = -overflowY; yReachedEnd = true; }
|
||||
}
|
||||
if (xReachedEnd && yReachedEnd) cycleComplete_ = true;
|
||||
}
|
||||
|
||||
if (xReachedEnd && yReachedEnd) {
|
||||
cycleComplete_ = true;
|
||||
}
|
||||
|
||||
renderer->drawMedia(buffer, media, 1.0f, offsetX_, offsetY_, scale);
|
||||
renderer->drawMedia(buffer, media, 1.0f, state_.offsetX, state_.offsetY, state_.scale);
|
||||
return cycleComplete_;
|
||||
}
|
||||
|
||||
private:
|
||||
float offsetX_, offsetY_;
|
||||
AnimationState state_;
|
||||
bool cycleComplete_;
|
||||
|
||||
float getBaseScale(MediaAsset& media, int surfaceWidth, int surfaceHeight) {
|
||||
// (PanAnimation과 중복되지만, 각 클래스의 독립성을 위해 포함)
|
||||
float getBaseScale(MediaAsset& media, int surfaceWidth, int surfaceHeight) { /* PanAnimation과 동일 */
|
||||
if (media.getHeight() == 0 || surfaceHeight == 0) return 1.0f;
|
||||
float scale;
|
||||
if ((static_cast<float>(media.getWidth()) / media.getHeight()) > ((float)surfaceWidth / surfaceHeight)) {
|
||||
scale = (float)surfaceHeight / media.getHeight();
|
||||
@ -149,35 +146,67 @@ private:
|
||||
|
||||
|
||||
// ====================================================================
|
||||
// --- ZOOM 애니메이션 ---
|
||||
// --- ZOOM 애니메이션 (수정) ---
|
||||
// ====================================================================
|
||||
class ZoomAnimation : public AnimationStrategy {
|
||||
public:
|
||||
ZoomAnimation(float speed) : AnimationStrategy(speed) { reset(); }
|
||||
|
||||
void reset() override {
|
||||
scaleMultiplier_ = 1.0f;
|
||||
state_ = AnimationState();
|
||||
zoomMultiplier_ = 1.0f; // 줌 배율은 1.0에서 시작
|
||||
cycleComplete_ = false;
|
||||
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 {
|
||||
if (!cycleComplete_) {
|
||||
scaleMultiplier_ += 0.0005f * animationSpeed_ * zoomDirection_;
|
||||
if (zoomDirection_ == 1 && scaleMultiplier_ >= 1.2f) { scaleMultiplier_ = 1.2f; zoomDirection_ = -1; }
|
||||
else if (zoomDirection_ == -1 && scaleMultiplier_ <= 1.0f) { scaleMultiplier_ = 1.0f; zoomDirection_ = 1; cycleComplete_ = true; }
|
||||
// 줌 배율만 업데이트 (1.0 ~ 1.2)
|
||||
zoomMultiplier_ += 0.0004f * animationSpeed_ * zoomDirection_;
|
||||
if (zoomDirection_ == 1 && zoomMultiplier_ >= zoom_max) {
|
||||
zoomDirection_ = -1;
|
||||
} else if (zoomMultiplier_ <= 1.0 && zoomDirection_ == -1){
|
||||
cycleComplete_ = true; // 최대 확대 시 사이클 완료
|
||||
}
|
||||
}
|
||||
|
||||
// ZOOM은 중앙 정렬을 기본으로 함
|
||||
// 1. 화면에 꽉 차는 기본 스케일을 계산
|
||||
float 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_;
|
||||
}
|
||||
|
||||
private:
|
||||
float scaleMultiplier_;
|
||||
AnimationState state_;
|
||||
float zoomMultiplier_; // 애니메이션 배율만 따로 관리
|
||||
int zoomDirection_;
|
||||
bool cycleComplete_;
|
||||
};
|
||||
@ -191,10 +220,23 @@ public:
|
||||
PageTurnAnimation(float speed, long long delay) : AnimationStrategy(speed), delayMs_(delay) { reset(); }
|
||||
|
||||
void reset() override {
|
||||
state_ = AnimationState();
|
||||
cycleComplete_ = false;
|
||||
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 {
|
||||
if (!cycleComplete_) {
|
||||
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;
|
||||
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_;
|
||||
}
|
||||
private:
|
||||
AnimationState state_;
|
||||
long long delayMs_;
|
||||
std::chrono::steady_clock::time_point startTime_;
|
||||
bool cycleComplete_;
|
||||
@ -222,15 +267,31 @@ private:
|
||||
// ====================================================================
|
||||
class NoneAnimation : public AnimationStrategy {
|
||||
public:
|
||||
NoneAnimation(float speed) : AnimationStrategy(speed) {}
|
||||
void reset() override {} // 아무것도 안 함
|
||||
NoneAnimation(float speed) : AnimationStrategy(speed) { reset(); }
|
||||
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;
|
||||
renderer->calculateFitScaleAndOffset(media, surfaceWidth, surfaceHeight, baseScale, baseOffsetX, baseOffsetY);
|
||||
renderer->drawMedia(buffer, media, 1.0f, baseOffsetX, baseOffsetY, baseScale);
|
||||
|
||||
return true; // 즉시 완료
|
||||
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 {
|
||||
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_;
|
||||
};
|
||||
@ -2,21 +2,47 @@
|
||||
#include <android/native_window.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 {
|
||||
public:
|
||||
virtual ~AnimationStrategy() = default;
|
||||
|
||||
/**
|
||||
* @brief 애니메이션 효과를 직접 렌더링 버퍼에 그립니다.
|
||||
* @return 애니메이션 한 사이클이 완료되었으면 true
|
||||
* @brief 매 프레임마다 호출되어 애니메이션을 직접 그리고, 상태를 반환합니다.
|
||||
* @return 애니메이션 한 사이클이 완료되었으면 true를 반환합니다.
|
||||
*/
|
||||
virtual bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& media, int surfaceWidth, int surfaceHeight) = 0;
|
||||
|
||||
/**
|
||||
* @brief 애니메이션 내부 상태를 초기화합니다.
|
||||
*/
|
||||
virtual void reset() = 0;
|
||||
|
||||
/**
|
||||
* @brief 현재 애니메이션 상태를 반환합니다.
|
||||
*/
|
||||
virtual AnimationState getState() const = 0;
|
||||
|
||||
// --- ⬇️ 자신의 시작 상태를 알려주는 함수 추가 ⬇️ ---
|
||||
virtual AnimationState getStartState(Renderer* renderer, MediaAsset& media, int surfaceWidth, int surfaceHeight) = 0;
|
||||
|
||||
protected:
|
||||
// 생성자에서 애니메이션 속도를 받아 저장합니다.
|
||||
AnimationStrategy(float speed) : animationSpeed_(speed) {}
|
||||
float animationSpeed_;
|
||||
};
|
||||
@ -12,171 +12,279 @@
|
||||
#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; // .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() {
|
||||
release();
|
||||
}
|
||||
|
||||
// [수정] 파일 디스크립터를 받는 load 함수 구현
|
||||
// --- ⬇️ load(int fd) 함수 수정 ⬇️ ---
|
||||
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.");
|
||||
|
||||
// 먼저 비디오로 로딩 시도 (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;
|
||||
}
|
||||
|
||||
// path 기반 load 함수는 그대로 유지
|
||||
bool MediaAsset::load(const std::string& path) {
|
||||
release();
|
||||
size_t dotPos = path.find_last_of('.');
|
||||
if (dotPos == std::string::npos) {
|
||||
LOGE("Load failed: Could not find file extension in path: %s", path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t dotPos = path.find_last_of('.');
|
||||
|
||||
// '/proc/self/fd/...' 경로처럼 확장자가 없는 경우
|
||||
if (dotPos == std::string::npos) {
|
||||
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);
|
||||
|
||||
const std::vector<std::string> videoExts = {"mp4", "mkv", "avi", "mov", "webm"};
|
||||
const std::vector<std::string> videoExts = {"mp4", "mkv", "avi", "mov", "webm","gif"};
|
||||
const std::vector<std::string> imageExts = {"jpg", "jpeg", "png", "bmp", "webp"};
|
||||
|
||||
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) { // [수정] 이미지 확장자일 경우 이미지만 시도
|
||||
if (std::find(imageExts.begin(), imageExts.end(), extension) != imageExts.end()) {
|
||||
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());
|
||||
return false;
|
||||
}
|
||||
|
||||
// stb_image.h를 사용하여 이미지를 로드하는 함수
|
||||
|
||||
bool MediaAsset::loadImageWithStb(const std::string& path) {
|
||||
type_ = Type::IMAGE;
|
||||
// 4 채널(RGBA)로 강제 변환하여 로드합니다.
|
||||
imageData_ = stbi_load(path.c_str(), &width_, &height_, nullptr, 4);
|
||||
if (!imageData_) {
|
||||
LOGE("Failed to load image with stb_image: %s", stbi_failure_reason());
|
||||
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;
|
||||
}
|
||||
|
||||
// FFmpeg을 사용하여 비디오를 로드하는 함수
|
||||
bool MediaAsset::loadVideoWithFFmpeg(const std::string& path) {
|
||||
|
||||
// --- ⬇️ loadVideoWithFFmpeg 함수를 오버로딩하여 Custom I/O 로직 추가 ⬇️ ---
|
||||
bool MediaAsset::loadVideoWithFFmpeg(int fd) {
|
||||
type_ = Type::VIDEO;
|
||||
|
||||
// 1. 비디오 파일 열기
|
||||
if (avformat_open_input(&fmtCtx_, path.c_str(), nullptr, nullptr) != 0) {
|
||||
LOGE("Could not open video file: %s", path.c_str()); // 오타 수정: c_st() -> c_str()
|
||||
release();
|
||||
// 1. fd를 FILE* 스트림으로 변환
|
||||
FILE* file = fdopen(fd, "rb");
|
||||
if (!file) {
|
||||
LOGE("Custom I/O: Failed to fdopen for fd: %d", fd);
|
||||
close(fd); // fdopen 실패 시 fd를 직접 닫음
|
||||
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) {
|
||||
LOGE("Could not find stream information for %s", path.c_str());
|
||||
release();
|
||||
return false;
|
||||
LOGE("Could not find stream information");
|
||||
release(); return false;
|
||||
}
|
||||
|
||||
// 3. 최적의 비디오 스트림 찾기
|
||||
const AVCodec* codec = nullptr;
|
||||
videoStreamIdx_ = av_find_best_stream(fmtCtx_, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
|
||||
if (videoStreamIdx_ < 0) {
|
||||
LOGE("Could not find a video stream in %s", path.c_str());
|
||||
release();
|
||||
return false;
|
||||
LOGE("Could not find a video stream");
|
||||
release(); return false;
|
||||
}
|
||||
|
||||
// 4. 코덱 컨텍스트 준비
|
||||
codecCtx_ = avcodec_alloc_context3(codec);
|
||||
if (!codecCtx_ || avcodec_parameters_to_context(codecCtx_, fmtCtx_->streams[videoStreamIdx_]->codecpar) < 0) {
|
||||
LOGE("Failed to create codec context for %s", path.c_str());
|
||||
release();
|
||||
return false;
|
||||
LOGE("Failed to create codec context");
|
||||
release(); return false;
|
||||
}
|
||||
|
||||
// 5. 코덱 열기
|
||||
if (avcodec_open2(codecCtx_, codec, nullptr) < 0) {
|
||||
LOGE("Could not open codec for %s", path.c_str());
|
||||
release();
|
||||
return false;
|
||||
LOGE("Could not open codec");
|
||||
release(); return false;
|
||||
}
|
||||
|
||||
// 6. 프레임 및 패킷 할당
|
||||
frame_ = av_frame_alloc();
|
||||
packet_ = av_packet_alloc();
|
||||
if (!frame_ || !packet_) {
|
||||
LOGE("Could not allocate frame or packet for %s", path.c_str());
|
||||
release();
|
||||
return false;
|
||||
LOGE("Could not allocate frame or packet");
|
||||
release(); return false;
|
||||
}
|
||||
|
||||
// 7. RGBA 변환을 위한 SwsContext 준비
|
||||
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
|
||||
);
|
||||
swsCtx_ = sws_getContext(width_, height_, codecCtx_->pix_fmt, width_, height_, AV_PIX_FMT_RGBA, SWS_BILINEAR, nullptr, nullptr, nullptr);
|
||||
if (!swsCtx_) {
|
||||
LOGE("Could not create SwsContext for %s", path.c_str());
|
||||
release();
|
||||
return false;
|
||||
LOGE("Could not create SwsContext");
|
||||
release(); return false;
|
||||
}
|
||||
|
||||
// 8. RGBA 픽셀 데이터를 담을 버퍼 할당
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// 기존의 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() {
|
||||
// 이미지 데이터 해제
|
||||
if (type_ == Type::IMAGE && imageData_) {
|
||||
stbi_image_free(imageData_);
|
||||
}
|
||||
imageData_ = nullptr;
|
||||
|
||||
// FFmpeg 비디오 자원 해제
|
||||
if (packet_) av_packet_free(&packet_);
|
||||
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 (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_);
|
||||
|
||||
packet_ = nullptr;
|
||||
@ -184,8 +292,6 @@ void MediaAsset::release() {
|
||||
codecCtx_ = nullptr;
|
||||
fmtCtx_ = nullptr;
|
||||
swsCtx_ = nullptr;
|
||||
|
||||
// 공통 변수 초기화
|
||||
type_ = Type::UNKNOWN;
|
||||
width_ = 0;
|
||||
height_ = 0;
|
||||
@ -201,50 +307,3 @@ bool MediaAsset::isValid() const {
|
||||
}
|
||||
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;
|
||||
}
|
||||
@ -21,31 +21,30 @@ public:
|
||||
MediaAsset() = default;
|
||||
~MediaAsset();
|
||||
|
||||
// 복사를 방지하고 자원의 소유권을 효율적으로 이전하기 위한 이동 생성자 및 연산자
|
||||
MediaAsset(MediaAsset&& other) noexcept;
|
||||
MediaAsset& operator=(MediaAsset&& other) noexcept;
|
||||
|
||||
// 파일을 로드하는 메인 함수 (오버로딩)
|
||||
bool load(const std::string& path);
|
||||
bool load(int fd);
|
||||
|
||||
// 모든 자원을 해제하는 함수
|
||||
void release();
|
||||
|
||||
// 현재 미디어가 유효한지 확인
|
||||
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_; }
|
||||
// const 객체는 const 포인터를 반환하도록 수정
|
||||
const uint8_t* getImageData() const { return imageData_; }
|
||||
|
||||
// 기존: 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_; }
|
||||
@ -54,20 +53,23 @@ public:
|
||||
int getVideoStreamIndex() const { return videoStreamIdx_; }
|
||||
|
||||
private:
|
||||
// ... (이하 동일)
|
||||
bool loadInternal(const std::string& path);
|
||||
// --- ⬇️ 수정된 부분: int fd를 받는 함수 선언 추가 ⬇️ ---
|
||||
bool loadVideoWithFFmpeg(const std::string& path);
|
||||
bool loadVideoWithFFmpeg(int fd); // Custom I/O를 위한 오버로딩
|
||||
bool loadImageWithStb(const std::string& path);
|
||||
// --- 수정 끝 ---
|
||||
|
||||
Type type_ = Type::UNKNOWN;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
|
||||
uint8_t* imageData_ = nullptr;
|
||||
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_;
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
#include "Renderer.h"
|
||||
#include "AnimationStrategy.cpp"
|
||||
#include "AnimationStrategy.h"
|
||||
#include "TransitionStrategy.cpp"
|
||||
#include <android/log.h>
|
||||
#include <algorithm>
|
||||
@ -16,85 +17,42 @@ extern void callNextMediaCallback();
|
||||
// ====================================================================
|
||||
// 생성자, 소멸자 및 설정(Setter) 함수들
|
||||
// ====================================================================
|
||||
|
||||
Renderer::Renderer() {
|
||||
randomEngine_.seed(std::chrono::high_resolution_clock::now().time_since_epoch().count());
|
||||
setAnimationMode(static_cast<int>(AnimationMode::PAN));
|
||||
setTransitionMode(static_cast<int>(TransitionMode::FADE));
|
||||
}
|
||||
|
||||
Renderer::~Renderer() {
|
||||
release();
|
||||
}
|
||||
|
||||
void Renderer::release() {
|
||||
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;
|
||||
}
|
||||
Renderer::~Renderer() { release(); }
|
||||
void Renderer::release() { 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) {
|
||||
configuredAnimationMode_ = (mode >= 0 && mode <= static_cast<int>(AnimationMode::PAGE_TURN))
|
||||
? static_cast<AnimationMode>(mode)
|
||||
: AnimationMode::PAN;
|
||||
configuredAnimationMode_ = (mode >= 0 && mode <= static_cast<int>(AnimationMode::PAGE_TURN)) ? static_cast<AnimationMode>(mode) : AnimationMode::PAN;
|
||||
determineActiveAnimationMode();
|
||||
}
|
||||
|
||||
void Renderer::setTransitionMode(int mode) {
|
||||
configuredTransitionMode_ = (mode >= 0 && mode <= static_cast<int>(TransitionMode::MOSAIC))
|
||||
? static_cast<TransitionMode>(mode)
|
||||
: TransitionMode::FADE;
|
||||
configuredTransitionMode_ = (mode >= 0 && mode <= static_cast<int>(TransitionMode::MOSAIC)) ? static_cast<TransitionMode>(mode) : TransitionMode::FADE;
|
||||
}
|
||||
|
||||
void Renderer::determineActiveAnimationMode() {
|
||||
AnimationMode modeToSetActive;
|
||||
if (configuredAnimationMode_ == AnimationMode::RANDOM) {
|
||||
static const std::vector<AnimationMode> availableModes = {
|
||||
AnimationMode::PAN, AnimationMode::ZOOM, AnimationMode::NONE,
|
||||
AnimationMode::PAN_ONE_WAY, AnimationMode::PAGE_TURN
|
||||
};
|
||||
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);
|
||||
modeToSetActive = availableModes[dist(randomEngine_)];
|
||||
} else {
|
||||
modeToSetActive = configuredAnimationMode_;
|
||||
}
|
||||
activeAnimationMode_ = modeToSetActive;
|
||||
|
||||
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<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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,68 +61,133 @@ void Renderer::determineActiveAnimationMode() {
|
||||
// ====================================================================
|
||||
|
||||
void Renderer::handleAnimationState(ANativeWindow_Buffer& buffer, int surfaceWidth, int surfaceHeight) {
|
||||
// 안전장치: 애니메이션 전문가가 고용되지 않았다면 작업을 중단
|
||||
if (!animationStrategy_) {
|
||||
// 이 경우엔 그냥 검은 화면만 그림
|
||||
memset(buffer.bits, 0, buffer.stride * buffer.height * sizeof(uint32_t));
|
||||
return;
|
||||
}
|
||||
if (!animationStrategy_) return;
|
||||
|
||||
// 1. 위임: 현재 애니메이션 전문가에게 모든 계산과 그리기를 맡기고, 완료 여부만 보고받음
|
||||
// (이 execute 함수 내부에서 배경을 지우고, 좌표를 계산하고, drawMedia를 호출하는 모든 작업을 수행함)
|
||||
bool isCycleComplete = animationStrategy_->execute(this, buffer, currentMedia_, surfaceWidth, surfaceHeight);
|
||||
|
||||
|
||||
// 2. 상태 전환: 애니메이션이 끝났고, 다음 미디어가 대기 중이라면 '전환' 상태로 넘어감
|
||||
if (isCycleComplete && nextMedia_.isValid()) {
|
||||
lastAnimationState_ = animationStrategy_->getState();
|
||||
predictedNextAnimationMode_ = predictNextAnimationMode();
|
||||
currentState_ = RenderState::TRANSITIONING;
|
||||
transitionStartTime_ = std::chrono::steady_clock::now();
|
||||
|
||||
// 사용자가 설정한 전환 모드를 확인
|
||||
TransitionMode transModeToUse = configuredTransitionMode_;
|
||||
if (transModeToUse == TransitionMode::RANDOM) {
|
||||
// RANDOM일 경우, FADE, SLIDE, MOSAIC 중에서 무작위로 선택
|
||||
std::uniform_int_distribution<int> dist(0, 2);
|
||||
int randomChoice = dist(randomEngine_);
|
||||
if (randomChoice == 0) transModeToUse = TransitionMode::FADE;
|
||||
else if (randomChoice == 1) transModeToUse = TransitionMode::SLIDE;
|
||||
else transModeToUse = TransitionMode::MOSAIC;
|
||||
transModeToUse = static_cast<TransitionMode>(dist(randomEngine_));
|
||||
}
|
||||
|
||||
// 결정된 모드에 맞는 '전환 전문가' 객체를 생성
|
||||
if (transModeToUse == TransitionMode::SLIDE) {
|
||||
transitionStrategy_ = std::make_unique<SlideTransition>(fadeDurationMs_, surfaceWidth);
|
||||
} else if (transModeToUse == TransitionMode::MOSAIC) {
|
||||
transitionStrategy_ = std::make_unique<MosaicTransition>(fadeDurationMs_, 20, 32, randomEngine_);
|
||||
} else { // 기본값은 FADE
|
||||
} else {
|
||||
transitionStrategy_ = std::make_unique<FadeTransition>(fadeDurationMs_);
|
||||
}
|
||||
|
||||
// 새로운 전환 전문가의 상태를 초기화
|
||||
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) {
|
||||
|
||||
if (!transitionStrategy_ || !nextMedia_.isValid()) {
|
||||
currentState_ = RenderState::ANIMATING;
|
||||
if(animationStrategy_) animationStrategy_->reset();
|
||||
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();
|
||||
long long elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - transitionStartTime_).count();
|
||||
bool isComplete = transitionStrategy_->isComplete(elapsed);
|
||||
|
||||
transitionStrategy_->execute(this, buffer, currentMedia_, nextMedia_, elapsed);
|
||||
bool isComplete = transitionStrategy_->execute(this, buffer, currentMedia_, lastAnimationState_, nextMedia_, destStartState, elapsed);
|
||||
|
||||
// 4. 상태 전환 체크
|
||||
if (isComplete) {
|
||||
currentMedia_ = std::move(nextMedia_);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
if (!media.isValid() || alpha <= 0.0f) return;
|
||||
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);
|
||||
|
||||
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 dstPixelValue = dstRow[x];
|
||||
|
||||
@ -9,47 +9,34 @@
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <memory> // for std::unique_ptr
|
||||
#include <memory>
|
||||
#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
|
||||
PAN = 0, ZOOM = 1, NONE = 2, RANDOM = 3,
|
||||
PAN_ONE_WAY = 4, PAGE_TURN = 5
|
||||
};
|
||||
|
||||
// 사용자가 설정할 수 있는 전환 효과 종류
|
||||
enum class TransitionMode {
|
||||
FADE = 0,
|
||||
SLIDE = 1,
|
||||
RANDOM = 2,
|
||||
MOSAIC = 3
|
||||
FADE = 0, SLIDE = 1, RANDOM = 2, MOSAIC = 3
|
||||
};
|
||||
|
||||
Renderer();
|
||||
~Renderer();
|
||||
|
||||
// --- Public API ---
|
||||
void setNextMedia(int fd);
|
||||
void renderFrame(ANativeWindow* window);
|
||||
void release();
|
||||
std::string getDebugInfo() const;
|
||||
|
||||
// --- 설정(Settings)을 위한 함수들 ---
|
||||
void setAnimationSpeed(float speed);
|
||||
void setAnimationMode(int mode);
|
||||
void setFadeDuration(int durationMs);
|
||||
void setPageTurnDelay(int delayMs);
|
||||
void setTransitionMode(int mode);
|
||||
|
||||
// --- 전략(Strategy) 객체들이 호출하는 헬퍼 함수들 ---
|
||||
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 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;
|
||||
|
||||
private:
|
||||
// --- 상태 머신(State Machine) ---
|
||||
enum class RenderState {
|
||||
ANIMATING,
|
||||
TRANSITIONING
|
||||
};
|
||||
enum class RenderState { ANIMATING, TRANSITIONING };
|
||||
RenderState currentState_ = RenderState::ANIMATING;
|
||||
bool animationCycleComplete_ = false; // <-- 이 플래그가 다시 필요
|
||||
// --- 상태별 처리를 위한 private 핸들러 함수 ---
|
||||
|
||||
void handleAnimationState(ANativeWindow_Buffer& buffer, int surfaceWidth, int surfaceHeight);
|
||||
void handleTransitionState(ANativeWindow_Buffer& buffer, int surfaceWidth, int surfaceHeight);
|
||||
|
||||
// --- 멤버 객체들 ---
|
||||
std::mutex renderMutex_;
|
||||
Preloader preloader_;
|
||||
MediaAsset currentMedia_;
|
||||
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;
|
||||
TransitionMode configuredTransitionMode_ = TransitionMode::FADE;
|
||||
AnimationMode activeAnimationMode_ = AnimationMode::PAN;
|
||||
@ -84,12 +67,11 @@ private:
|
||||
long long pageTurnDelayMs_ = 5000;
|
||||
float animationSpeed_ = 1.0f;
|
||||
|
||||
// --- 상태(State) 관리 변수 ---
|
||||
std::chrono::steady_clock::time_point transitionStartTime_;
|
||||
AnimationState getStartStateForMode(AnimationMode mode, int surfaceWidth, int surfaceHeight);
|
||||
AnimationMode predictNextAnimationMode();
|
||||
|
||||
// --- 랜덤 기능 ---
|
||||
std::chrono::steady_clock::time_point transitionStartTime_;
|
||||
std::mt19937 randomEngine_;
|
||||
|
||||
// --- private 헬퍼 함수 ---
|
||||
void determineActiveAnimationMode();
|
||||
};
|
||||
@ -1,74 +1,68 @@
|
||||
#include "TransitionStrategy.h"
|
||||
#include "Renderer.h"
|
||||
#include <algorithm> // for std::clamp
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <numeric> // for std::iota
|
||||
#include <random> // for std::shuffle
|
||||
#include <numeric>
|
||||
#include <random>
|
||||
|
||||
// ====================================================================
|
||||
// --- 페이드 전환 효과 ---
|
||||
// ====================================================================
|
||||
class FadeTransition : public TransitionStrategy {
|
||||
public:
|
||||
FadeTransition(long long duration) : TransitionStrategy(duration) {}
|
||||
void reset() override {}
|
||||
bool isComplete(long long elapsedMs) const override { return elapsedMs >= durationMs_; }
|
||||
|
||||
bool isComplete(long long elapsedMs) const override {
|
||||
return elapsedMs >= durationMs_;
|
||||
}
|
||||
bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer,
|
||||
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);
|
||||
|
||||
// 1. 사라지는 이미지(source)의 올바른 위치와 크기를 계산
|
||||
float srcScale, srcOffsetX, srcOffsetY;
|
||||
renderer->calculateFitScaleAndOffset(source, buffer.width, buffer.height, srcScale, srcOffsetX, srcOffsetY);
|
||||
|
||||
// 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);
|
||||
// source는 전달받은 마지막 상태(sourceEnd) 그대로 페이드 아웃
|
||||
renderer->drawMedia(buffer, source, 1.0f - progress, sourceEnd.offsetX, sourceEnd.offsetY, sourceEnd.scale);
|
||||
// dest는 전달받은 시작 상태(destStart) 그대로 페이드 인
|
||||
renderer->drawMedia(buffer, dest, progress, destStart.offsetX, destStart.offsetY, destStart.scale);
|
||||
|
||||
return isComplete(elapsedMs);
|
||||
}
|
||||
};
|
||||
|
||||
// --- 슬라이드 전환 효과 ---
|
||||
// --- ⬇️ 슬라이드 전환 효과 (수정) ⬇️ ---
|
||||
class SlideTransition : public TransitionStrategy {
|
||||
public:
|
||||
SlideTransition(long long duration, int surfaceWidth)
|
||||
: TransitionStrategy(duration), surfaceWidth_(surfaceWidth) {}
|
||||
void reset() override {}
|
||||
bool isComplete(long long elapsedMs) const override { return elapsedMs >= durationMs_; }
|
||||
|
||||
bool isComplete(long long elapsedMs) const override {
|
||||
return elapsedMs >= durationMs_;
|
||||
}
|
||||
bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer,
|
||||
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 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 destSlideX = surfaceWidth_ * (1.0f - easeProgress);
|
||||
|
||||
// 3. 기본 위치에 슬라이드 이동거리를 더해서 최종 위치 결정
|
||||
renderer->drawMedia(buffer, source, 1.0f, srcBaseOffsetX + sourceSlideX, srcBaseOffsetY, srcScale);
|
||||
renderer->drawMedia(buffer, dest, 1.0f, destBaseOffsetX + destSlideX, destBaseOffsetY, destScale);
|
||||
// source는 마지막 위치(sourceEnd)에서 왼쪽으로 슬라이드 아웃
|
||||
renderer->drawMedia(buffer, source, 1.0f, sourceEnd.offsetX + sourceSlideX, sourceEnd.offsetY, sourceEnd.scale);
|
||||
// dest는 자신의 시작 위치(destStart) 오른쪽 밖에서 중앙으로 슬라이드 인
|
||||
renderer->drawMedia(buffer, dest, 1.0f, destStart.offsetX + destSlideX, destStart.offsetY, destStart.scale);
|
||||
|
||||
return isComplete(elapsedMs);
|
||||
}
|
||||
private:
|
||||
int surfaceWidth_;
|
||||
};
|
||||
|
||||
|
||||
// --- ⬇️ 모자이크 전환 효과 (전체 구현) ⬇️ ---
|
||||
// ====================================================================
|
||||
// --- 모자이크 전환 효과 ---
|
||||
// ====================================================================
|
||||
class MosaicTransition : public TransitionStrategy {
|
||||
public:
|
||||
MosaicTransition(long long duration, int tilesX, int tilesY, std::mt19937& randomEngine)
|
||||
@ -76,14 +70,11 @@ public:
|
||||
reset();
|
||||
}
|
||||
|
||||
// 전환 시작 시 타일 순서를 섞음
|
||||
void reset() override {
|
||||
int totalTiles = tilesX_ * tilesY_;
|
||||
tileOrder_.resize(totalTiles);
|
||||
std::iota(tileOrder_.begin(), tileOrder_.end(), 0); // 0, 1, 2, ... 순서로 벡터 채우기
|
||||
std::shuffle(tileOrder_.begin(), tileOrder_.end(), randomEngine_); // 순서 섞기
|
||||
|
||||
// 빠른 조회를 위한 역순 조회 테이블 생성
|
||||
std::iota(tileOrder_.begin(), tileOrder_.end(), 0);
|
||||
std::shuffle(tileOrder_.begin(), tileOrder_.end(), randomEngine_);
|
||||
revealOrder_.resize(totalTiles);
|
||||
for(size_t i = 0; i < tileOrder_.size(); ++i) {
|
||||
revealOrder_[tileOrder_[i]] = i;
|
||||
@ -94,67 +85,57 @@ public:
|
||||
return elapsedMs >= durationMs_;
|
||||
}
|
||||
|
||||
// 모자이크 효과를 픽셀 단위로 직접 그림
|
||||
void execute(Renderer* renderer, ANativeWindow_Buffer& buffer, MediaAsset& source, MediaAsset& dest, long long elapsedMs) override {
|
||||
bool execute(Renderer* renderer, ANativeWindow_Buffer& buffer,
|
||||
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);
|
||||
int totalTiles = tilesX_ * tilesY_;
|
||||
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* destPixelData = dest.getType() == MediaAsset::Type::IMAGE ? dest.getImageData() : dest.getRgbBuffer().data();
|
||||
|
||||
if (!srcPixelData || !destPixelData) return;
|
||||
if (!srcPixelData || !destPixelData) return isComplete(elapsedMs);
|
||||
|
||||
uint32_t* dstPixels = (uint32_t*)buffer.bits;
|
||||
int dstStride = buffer.stride;
|
||||
float tileWidth = (float)buffer.width / tilesX_;
|
||||
float tileHeight = (float)buffer.height / tilesY_;
|
||||
|
||||
// 화면의 모든 픽셀을 순회
|
||||
for (int y = 0; y < buffer.height; ++y) {
|
||||
uint32_t* dstRow = dstPixels + y * dstStride;
|
||||
for (int x = 0; x < buffer.width; ++x) {
|
||||
// 현재 픽셀이 속한 타일의 인덱스 계산
|
||||
int tileX = static_cast<int>(x / tileWidth);
|
||||
int tileY = static_cast<int>(y / tileHeight);
|
||||
int tileIndex = tileY * tilesX_ + tileX;
|
||||
|
||||
// 이 타일이 몇 번째로 드러나야 하는지(rank) 확인
|
||||
int rank = revealOrder_[tileIndex];
|
||||
|
||||
const uint8_t* finalPixelData;
|
||||
if (rank < tilesToReveal) {
|
||||
// 드러나야 할 타일이면, 새 이미지(dest)에서 픽셀을 가져옴
|
||||
int srcX = static_cast<int>((x + destOffsetX) / destScale);
|
||||
int srcY = static_cast<int>((y + destOffsetY) / destScale);
|
||||
// --- ⬇️ 수정된 부분: (x - offsetX) 공식 적용 ⬇️ ---
|
||||
int srcX = static_cast<int>((x - destStart.offsetX) / destStart.scale);
|
||||
int srcY = static_cast<int>((y - destStart.offsetY) / destStart.scale);
|
||||
if (srcX >= 0 && srcX < dest.getWidth() && srcY >= 0 && srcY < dest.getHeight()) {
|
||||
finalPixelData = &destPixelData[(srcY * dest.getWidth() + srcX) * 4];
|
||||
} else { continue; }
|
||||
} else {
|
||||
// 아직 드러나지 않은 타일이면, 이전 이미지(source)에서 픽셀을 가져옴
|
||||
int srcX = static_cast<int>((x + srcOffsetX) / srcScale);
|
||||
int srcY = static_cast<int>((y + srcOffsetY) / srcScale);
|
||||
// --- ⬇️ 수정된 부분: (x - offsetX) 공식 적용 ⬇️ ---
|
||||
int srcX = static_cast<int>((x - sourceEnd.offsetX) / sourceEnd.scale);
|
||||
int srcY = static_cast<int>((y - sourceEnd.offsetY) / sourceEnd.scale);
|
||||
if (srcX >= 0 && srcX < source.getWidth() && srcY >= 0 && srcY < source.getHeight()) {
|
||||
finalPixelData = &srcPixelData[(srcY * source.getWidth() + srcX) * 4];
|
||||
} else { continue; }
|
||||
}
|
||||
|
||||
// 최종 픽셀을 버퍼에 씀
|
||||
dstRow[x] = (0xFF << 24) | (finalPixelData[0] << 16) | (finalPixelData[1] << 8) | finalPixelData[2];
|
||||
}
|
||||
}
|
||||
return isComplete(elapsedMs);
|
||||
}
|
||||
|
||||
private:
|
||||
int tilesX_, tilesY_;
|
||||
std::vector<int> tileOrder_;
|
||||
std::vector<int> revealOrder_; // 역순 조회 테이블
|
||||
std::vector<int> revealOrder_;
|
||||
std::mt19937& randomEngine_;
|
||||
};
|
||||
@ -1,10 +1,13 @@
|
||||
#pragma once
|
||||
#include <android/native_window.h>
|
||||
#include "MediaAsset.h"
|
||||
#include "AnimationStrategy.h" // AnimationState를 사용하기 위해 포함
|
||||
|
||||
// Renderer 클래스에 대한 전방 선언 (순환 참조 방지)
|
||||
class Renderer;
|
||||
class Renderer; // 전방 선언
|
||||
|
||||
/**
|
||||
* @brief 모든 전환 효과 전문가(Strategy) 클래스의 기반이 되는 인터페이스입니다.
|
||||
*/
|
||||
class TransitionStrategy {
|
||||
public:
|
||||
virtual ~TransitionStrategy() = default;
|
||||
@ -19,14 +22,14 @@ public:
|
||||
|
||||
/**
|
||||
* @brief 전환 효과를 직접 렌더링 버퍼에 그립니다.
|
||||
* @param renderer Renderer 객체에 대한 포인터 (헬퍼 함수 호출용)
|
||||
* @param buffer 그림을 그릴 캔버스
|
||||
* @param source 사라지는 미디어
|
||||
* @param dest 나타나는 미디어
|
||||
* @param elapsedMs 전환 시작 후 경과 시간
|
||||
* @return 전환이 완료되었으면 true, 아니면 false
|
||||
* @param sourceEnd 사라지는 미디어의 최종 애니메이션 상태
|
||||
* @param destStart 나타나는 미디어의 시작 애니메이션 상태
|
||||
* @return 전환이 완료되었으면 true를 반환합니다.
|
||||
*/
|
||||
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;
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ class MyWallpaperService : WallpaperService() {
|
||||
private var isVisible = false
|
||||
private var isSurfaceValid = false
|
||||
|
||||
private val frameDelayMs = 1000L / 45L
|
||||
private val frameDelayMs = 1000L / 50L
|
||||
private val syncDelayMs = 250L // 상태 변경을 처리하기 전의 지연 시간
|
||||
private var frameCount = 0
|
||||
private val debugLogCheck = 240
|
||||
@ -183,11 +183,9 @@ class MyWallpaperService : WallpaperService() {
|
||||
nativeRenderer?.initialize() // nativeInit() -> initialize()
|
||||
nativeRenderer?.setFadeDuration(1500)
|
||||
nativeRenderer?.setTurnPageDuration(8000)
|
||||
nativeRenderer?.setAnimationMode(NativeRenderer.ANIMATION_MODE_PAN)
|
||||
nativeRenderer?.setTransitionMode(NativeRenderer.TRANSITION_MODE_FADE)
|
||||
nativeRenderer?.setAnimationSpeed(10.0f) // nativeSetAnimationSpeed -> setAnimationSpeed
|
||||
|
||||
|
||||
nativeRenderer?.setAnimationMode(NativeRenderer.ANIMATION_MODE_RANDOM)
|
||||
nativeRenderer?.setTransitionMode(NativeRenderer.TRANSITION_MODE_RANDOM)
|
||||
nativeRenderer?.setAnimationSpeed(2.0f)
|
||||
NativeRenderer.nativeSetNextMediaCallback(nextMediaCallback)
|
||||
|
||||
|
||||
@ -197,10 +195,7 @@ class MyWallpaperService : WallpaperService() {
|
||||
}
|
||||
// ... loadMediaFiles, nextMediaCallback, getFdFromPath는 이전과 동일 ...
|
||||
private fun loadMediaFiles() {
|
||||
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallPapers")
|
||||
if (!mediaDir.exists()) mediaDir.mkdirs()
|
||||
|
||||
val supportedExtensions = listOf("mp4", "mkv", "avi", "mov", "webm", "jpg", "jpeg", "png", "bmp", "webp")
|
||||
mediaFiles = mediaDir.listFiles()
|
||||
?.filter { supportedExtensions.contains(it.extension.lowercase()) }
|
||||
?.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 {
|
||||
override fun onNextMediaRequested() {
|
||||
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallPapers")
|
||||
|
||||
if (!mediaDir.exists()) mediaDir.mkdirs()
|
||||
|
||||
val supportedExtensions = listOf("mp4", "mkv", "avi", "mov", "webm", "jpg", "jpeg", "png", "bmp", "webp")
|
||||
|
||||
mediaFiles = mediaDir.listFiles()
|
||||
?.filter { supportedExtensions.contains(it.extension.lowercase()) }
|
||||
?.toList() ?: emptyList()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user