diff --git a/app/src/main/cpp/AnimationStrategy.cpp b/app/src/main/cpp/AnimationStrategy.cpp index b0888b79..730319c3 100644 --- a/app/src/main/cpp/AnimationStrategy.cpp +++ b/app/src/main/cpp/AnimationStrategy.cpp @@ -1,6 +1,6 @@ #include "AnimationStrategy.h" -#include "Renderer.h" // Renderer의 그리기 헬퍼 함수들을 사용하기 위해 포함 -#include // for std::max, std::clamp +#include "Renderer.h" +#include #include #include #include @@ -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(media.getWidth()); float mediaH = static_cast(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(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(media.getWidth()); float mediaH = static_cast(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(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(surfaceWidth) - (media.getWidth() * finalScale)) / 2.0f; + float finalOffsetY = (static_cast(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::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_; }; \ No newline at end of file diff --git a/app/src/main/cpp/AnimationStrategy.h b/app/src/main/cpp/AnimationStrategy.h index 9be04d3c..54bcddd3 100644 --- a/app/src/main/cpp/AnimationStrategy.h +++ b/app/src/main/cpp/AnimationStrategy.h @@ -2,21 +2,47 @@ #include #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_; }; \ No newline at end of file diff --git a/app/src/main/cpp/MediaAsset.cpp b/app/src/main/cpp/MediaAsset.cpp index 57e0310d..c03e9a6e 100644 --- a/app/src/main/cpp/MediaAsset.cpp +++ b/app/src/main/cpp/MediaAsset.cpp @@ -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 videoExts = {"mp4", "mkv", "avi", "mov", "webm"}; + const std::vector videoExts = {"mp4", "mkv", "avi", "mov", "webm","gif"}; const std::vector 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; @@ -200,51 +306,4 @@ bool MediaAsset::isValid() const { return fmtCtx_ != nullptr && codecCtx_ != nullptr && frame_ != nullptr && swsCtx_ != nullptr; } 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; } \ No newline at end of file diff --git a/app/src/main/cpp/MediaAsset.h b/app/src/main/cpp/MediaAsset.h index 16f03128..27ab51ef 100644 --- a/app/src/main/cpp/MediaAsset.h +++ b/app/src/main/cpp/MediaAsset.h @@ -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& getRgbBuffer() { return rgbBuffer_; } - // non-const 객체용 (기존 함수 유지) std::vector& getRgbBuffer() { return rgbBuffer_; } - // const 객체용 오버로딩 함수 추가 const std::vector& 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 rgbBuffer_; AVFormatContext* fmtCtx_ = nullptr; AVCodecContext* codecCtx_ = nullptr; AVFrame* frame_ = nullptr; AVPacket* packet_ = nullptr; SwsContext* swsCtx_ = nullptr; int videoStreamIdx_ = -1; - std::vector rgbBuffer_; }; \ No newline at end of file diff --git a/app/src/main/cpp/Renderer.cpp b/app/src/main/cpp/Renderer.cpp index 90c7aa69..ff6f3f2d 100644 --- a/app/src/main/cpp/Renderer.cpp +++ b/app/src/main/cpp/Renderer.cpp @@ -1,5 +1,6 @@ #include "Renderer.h" #include "AnimationStrategy.cpp" +#include "AnimationStrategy.h" #include "TransitionStrategy.cpp" #include #include @@ -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(AnimationMode::PAN)); setTransitionMode(static_cast(TransitionMode::FADE)); } - -Renderer::~Renderer() { - release(); -} - -void Renderer::release() { - std::lock_guard 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 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(AnimationMode::PAGE_TURN)) - ? static_cast(mode) - : AnimationMode::PAN; + configuredAnimationMode_ = (mode >= 0 && mode <= static_cast(AnimationMode::PAGE_TURN)) ? static_cast(mode) : AnimationMode::PAN; determineActiveAnimationMode(); } - void Renderer::setTransitionMode(int mode) { - configuredTransitionMode_ = (mode >= 0 && mode <= static_cast(TransitionMode::MOSAIC)) - ? static_cast(mode) - : TransitionMode::FADE; + configuredTransitionMode_ = (mode >= 0 && mode <= static_cast(TransitionMode::MOSAIC)) ? static_cast(mode) : TransitionMode::FADE; } void Renderer::determineActiveAnimationMode() { AnimationMode modeToSetActive; if (configuredAnimationMode_ == AnimationMode::RANDOM) { - static const std::vector availableModes = { - AnimationMode::PAN, AnimationMode::ZOOM, AnimationMode::NONE, - AnimationMode::PAN_ONE_WAY, AnimationMode::PAGE_TURN - }; + static const std::vector availableModes = { AnimationMode::PAN, AnimationMode::ZOOM, AnimationMode::NONE, AnimationMode::PAN_ONE_WAY, AnimationMode::PAGE_TURN }; std::uniform_int_distribution dist(0, availableModes.size() - 1); modeToSetActive = availableModes[dist(randomEngine_)]; } else { modeToSetActive = configuredAnimationMode_; } activeAnimationMode_ = modeToSetActive; - switch (activeAnimationMode_) { - case AnimationMode::PAN: - animationStrategy_ = std::make_unique(animationSpeed_); - break; - case AnimationMode::PAN_ONE_WAY: - animationStrategy_ = std::make_unique(animationSpeed_); - break; - case AnimationMode::ZOOM: - animationStrategy_ = std::make_unique(animationSpeed_); - break; - case AnimationMode::PAGE_TURN: - animationStrategy_ = std::make_unique(animationSpeed_, pageTurnDelayMs_); - break; - case AnimationMode::NONE: - default: - animationStrategy_ = std::make_unique(animationSpeed_); - break; - case AnimationMode::RANDOM: - break; + case AnimationMode::PAN: animationStrategy_ = std::make_unique(animationSpeed_); break; + case AnimationMode::PAN_ONE_WAY: animationStrategy_ = std::make_unique(animationSpeed_); break; + case AnimationMode::ZOOM: animationStrategy_ = std::make_unique(animationSpeed_); break; + case AnimationMode::PAGE_TURN: animationStrategy_ = std::make_unique(animationSpeed_, pageTurnDelayMs_); break; + case AnimationMode::NONE: default: animationStrategy_ = std::make_unique(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 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(dist(randomEngine_)); } - - // 결정된 모드에 맞는 '전환 전문가' 객체를 생성 if (transModeToUse == TransitionMode::SLIDE) { transitionStrategy_ = std::make_unique(fadeDurationMs_, surfaceWidth); } else if (transModeToUse == TransitionMode::MOSAIC) { transitionStrategy_ = std::make_unique(fadeDurationMs_, 20, 32, randomEngine_); - } else { // 기본값은 FADE + } else { transitionStrategy_ = std::make_unique(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 availableModes = { + AnimationMode::PAN, AnimationMode::ZOOM, AnimationMode::NONE, + AnimationMode::PAN_ONE_WAY, AnimationMode::PAGE_TURN + }; + std::uniform_int_distribution 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(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 tempNextStrategy; + switch (predictedNextAnimationMode_) { + case AnimationMode::PAN: + tempNextStrategy = std::make_unique(animationSpeed_); + break; + case AnimationMode::PAN_ONE_WAY: + tempNextStrategy = std::make_unique(animationSpeed_); + break; + case AnimationMode::ZOOM: + tempNextStrategy = std::make_unique(animationSpeed_); + break; + case AnimationMode::PAGE_TURN: + tempNextStrategy = std::make_unique(animationSpeed_, pageTurnDelayMs_); + break; + case AnimationMode::NONE: + default: + tempNextStrategy = std::make_unique(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(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(animationSpeed_); break; + case AnimationMode::PAN_ONE_WAY: animationStrategy_ = std::make_unique(animationSpeed_); break; + case AnimationMode::ZOOM: animationStrategy_ = std::make_unique(animationSpeed_); break; + case AnimationMode::PAGE_TURN: animationStrategy_ = std::make_unique(animationSpeed_, pageTurnDelayMs_); break; + case AnimationMode::NONE: + default: animationStrategy_ = std::make_unique(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(alpha * 255.0f); for (int y = 0; y < buffer.height; ++y) { - int srcY = static_cast((y + offsetY) / scale); + int srcY = static_cast((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((x + offsetX) / scale); + int srcX = static_cast((x - offsetX) / scale); if (srcX < 0 || srcX >= imgW) continue; const uint8_t* srcPixel = &pixelData[(srcY * imgW + srcX) * 4]; uint32_t dstPixelValue = dstRow[x]; diff --git a/app/src/main/cpp/Renderer.h b/app/src/main/cpp/Renderer.h index 54780a08..a55b8232 100644 --- a/app/src/main/cpp/Renderer.h +++ b/app/src/main/cpp/Renderer.h @@ -9,47 +9,34 @@ #include #include #include -#include // for std::unique_ptr +#include #include #include 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_; - std::unique_ptr transitionStrategy_; - // --- 설정값 저장 변수 --- + std::unique_ptr animationStrategy_; + AnimationMode predictedNextAnimationMode_; + std::unique_ptr 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(); }; \ No newline at end of file diff --git a/app/src/main/cpp/TransitionStrategy.cpp b/app/src/main/cpp/TransitionStrategy.cpp index 0459d088..87ad37df 100644 --- a/app/src/main/cpp/TransitionStrategy.cpp +++ b/app/src/main/cpp/TransitionStrategy.cpp @@ -1,74 +1,68 @@ #include "TransitionStrategy.h" #include "Renderer.h" -#include // for std::clamp +#include #include -#include // for std::iota -#include // for std::shuffle +#include +#include +// ==================================================================== // --- 페이드 전환 효과 --- +// ==================================================================== 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(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(x / tileWidth); int tileY = static_cast(y / tileHeight); int tileIndex = tileY * tilesX_ + tileX; - - // 이 타일이 몇 번째로 드러나야 하는지(rank) 확인 int rank = revealOrder_[tileIndex]; const uint8_t* finalPixelData; if (rank < tilesToReveal) { - // 드러나야 할 타일이면, 새 이미지(dest)에서 픽셀을 가져옴 - int srcX = static_cast((x + destOffsetX) / destScale); - int srcY = static_cast((y + destOffsetY) / destScale); + // --- ⬇️ 수정된 부분: (x - offsetX) 공식 적용 ⬇️ --- + int srcX = static_cast((x - destStart.offsetX) / destStart.scale); + int srcY = static_cast((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((x + srcOffsetX) / srcScale); - int srcY = static_cast((y + srcOffsetY) / srcScale); + // --- ⬇️ 수정된 부분: (x - offsetX) 공식 적용 ⬇️ --- + int srcX = static_cast((x - sourceEnd.offsetX) / sourceEnd.scale); + int srcY = static_cast((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 tileOrder_; - std::vector revealOrder_; // 역순 조회 테이블 + std::vector revealOrder_; std::mt19937& randomEngine_; }; \ No newline at end of file diff --git a/app/src/main/cpp/TransitionStrategy.h b/app/src/main/cpp/TransitionStrategy.h index c596a6ac..aa891f41 100644 --- a/app/src/main/cpp/TransitionStrategy.h +++ b/app/src/main/cpp/TransitionStrategy.h @@ -1,10 +1,13 @@ #pragma once #include #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; diff --git a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt index bf55339b..4cb14fcc 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/wall/MyWallpaperService.kt @@ -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()