This commit is contained in:
2025-08-27 15:09:05 +09:00
parent 57961d84de
commit a13f66d917
9896 changed files with 2193048 additions and 730 deletions
+1
View File
@@ -49,6 +49,7 @@ android {
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
signingConfigs {
+40 -34
View File
@@ -1,59 +1,65 @@
cmake_minimum_required(VERSION 3.18.1)
project("native_renderer")
cmake_minimum_required(VERSION 3.10.2)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# --- 1. FFmpeg 빌드 스크립트 실행 ---
# NOTE: 이 명령은 CMake 설정 단계에서 딱 한 번 실행되어 FFmpeg 라이브러리를 빌드합니다.
# ffmpeg 폴더에 있는 build_ffmpeg.sh 스크립트를 실행합니다.
execute_process(
COMMAND sh ../../../../ffmpeg/build_ffmpeg.sh
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE FFMPEG_BUILD_RESULT
)
project(native_renderer)
# 기본 로그 및 안드로이드 라이브러리
find_library(log-lib log)
find_library(android-lib android)
# ABI에 따라 jniLibs 경로 지정 (예: arm64-v8a, armeabi-v7a 등)
set(FFMPEG_LIB_DIR ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
message(STATUS "FFMPEG_LIB_DIR: ${FFMPEG_LIB_DIR}")
if(NOT FFMPEG_BUILD_RESULT EQUAL 0)
message(FATAL_ERROR "FFmpeg build script failed with exit code: ${FFMPEG_BUILD_RESULT}")
endif()
# --- 실행 끝 ---
# FFmpeg 헤더 경로 추가
include_directories(${FFMPEG_LIB_DIR}/include)
include_directories(${CMAKE_SOURCE_DIR}/include) # stb_image.h가 이 경로에 있다면
# --- 2. 빌드된 라이브러리 참조 ---
# NOTE: 이제 FFmpeg은 미리 빌드된 라이브러리(prebuilt)처럼 취급됩니다.
# jniLibs 폴더 경로 설정
set(JNI_LIBS_DIR ${CMAKE_SOURCE_DIR}/../../jniLibs/${ANDROID_ABI})
# FFmpeg 라이브러리들 임포트 선언 및 위치 지정
# 헤더 파일 경로 추가
include_directories(${JNI_LIBS_DIR}/include)
# 각 .so 파일을 IMPORTED 라이브러리로 추가
add_library(avformat SHARED IMPORTED)
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION
${FFMPEG_LIB_DIR}/libavformat.so)
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${JNI_LIBS_DIR}/libavformat.so)
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION
${FFMPEG_LIB_DIR}/libavcodec.so)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${JNI_LIBS_DIR}/libavcodec.so)
add_library(avutil SHARED IMPORTED)
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION
${FFMPEG_LIB_DIR}/libavutil.so)
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${JNI_LIBS_DIR}/libavutil.so)
add_library(swscale SHARED IMPORTED)
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION
${FFMPEG_LIB_DIR}/libswscale.so)
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION ${JNI_LIBS_DIR}/libswscale.so)
add_library(swresample SHARED IMPORTED)
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION
${FFMPEG_LIB_DIR}/libswresample.so)
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${JNI_LIBS_DIR}/libswresample.so)
add_library(avfilter SHARED IMPORTED)
set_target_properties(avfilter PROPERTIES IMPORTED_LOCATION
${FFMPEG_LIB_DIR}/libavfilter.so)
# 네이티브 렌더러 라이브러리 빌드
add_library(native_renderer SHARED native_renderer.cpp) # 실제 소스파일명 입력
# --- 3. 네이티브 렌더러 라이브러리 빌드 및 링크 ---
add_library(native_renderer SHARED
native_renderer.cpp
Renderer.cpp
Preloader.cpp
MediaAsset.cpp
)
find_library(log-lib log)
find_library(android-lib android)
find_library(nativewindow-lib nativewindow)
# 링크할 라이브러리 지정
target_link_libraries(native_renderer
avformat
avcodec
avutil
swscale
swresample
avfilter
${log-lib}
${android-lib}
)
${nativewindow-lib}
)
+198
View File
@@ -0,0 +1,198 @@
#include "MediaAsset.h"
#include <android/log.h>
#include <algorithm>
#include <vector>
#include <unistd.h> // close(fd) 함수를 사용하기 위해 추가
#define LOG_TAG "MediaAsset"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// NOTE: C++ JNI 단에서 파일 디스크립터(fd)의 소유권을 가지므로, 사용 후 반드시 닫아야 합니다.
bool MediaAsset::load(int fd) {
release(); // 새로 로드하기 전에 기존 자원 정리
if (fd < 0) {
LOGE("Invalid file descriptor received: %d", fd);
return false;
}
// NOTE: FFmpeg가 파일 디스크립터를 입력으로 받도록 "pipe:[fd]" 형식의 문자열을 생성합니다.
char path_from_fd[32];
snprintf(path_from_fd, sizeof(path_from_fd), "pipe:%d", fd);
// FFmpeg 로더 함수를 호출합니다.
loadMediaWithFFmpeg(path_from_fd);
// NOTE: 파일 디스크립터 사용이 끝났으므로 여기서 닫아줍니다. (누수 방지)
close(fd);
return isValid();
}
// NOTE: 이 함수는 이제 "pipe:[fd]" 형식의 경로를 받아 비디오와 이미지를 모두 처리합니다.
void MediaAsset::loadMediaWithFFmpeg(const std::string& path) {
// 1. 미디어 파일 열기
if (avformat_open_input(&fmtCtx_, path.c_str(), nullptr, nullptr) != 0) {
LOGE("Could not open media from path: %s", path.c_str());
return;
}
// 2. 스트림 정보 찾기
if (avformat_find_stream_info(fmtCtx_, nullptr) < 0) {
LOGE("Could not find stream information.");
release();
return;
}
// 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 media.");
release();
return;
}
AVStream* stream = fmtCtx_->streams[videoStreamIdx_];
// 4. 미디어 타입 결정 (비디오 vs 이미지)
if (stream->disposition & AV_DISPOSITION_ATTACHED_PIC || stream->duration <= 0) {
type_ = Type::IMAGE;
} else {
type_ = Type::VIDEO;
}
// 5. 코덱 컨텍스트 준비
codecCtx_ = avcodec_alloc_context3(codec);
if (!codecCtx_ || avcodec_parameters_to_context(codecCtx_, stream->codecpar) < 0) {
LOGE("Failed to create codec context.");
release();
return;
}
if (avcodec_open2(codecCtx_, codec, nullptr) < 0) {
LOGE("Could not open codec.");
release();
return;
}
// 6. 프레임 및 패킷 할당
frame_ = av_frame_alloc();
packet_ = av_packet_alloc();
if (!frame_ || !packet_) {
LOGE("Could not allocate frame or packet.");
release();
return;
}
// 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
);
if (!swsCtx_) {
LOGE("Could not create SwsContext.");
release();
return;
}
// 8. RGBA 픽셀 데이터를 담을 버퍼 할당
rgbBuffer_.resize(width_ * height_ * 4);
// 9. 이미지일 경우, 첫 프레임을 미리 디코딩하여 버퍼에 저장
if (type_ == Type::IMAGE) {
if (av_read_frame(fmtCtx_, packet_) >= 0 && packet_->stream_index == videoStreamIdx_) {
if (avcodec_send_packet(codecCtx_, packet_) == 0) {
if (avcodec_receive_frame(codecCtx_, frame_) == 0) {
uint8_t* dst[4] = { rgbBuffer_.data(), nullptr, nullptr, nullptr };
int dstStride_arr[4] = { width_ * 4, 0, 0, 0 };
sws_scale(swsCtx_, frame_->data, frame_->linesize, 0, height_, dst, dstStride_arr);
imageData_ = rgbBuffer_.data(); // imageData_가 버퍼를 가리키도록 설정
}
}
}
av_packet_unref(packet_);
// 이미지는 첫 프레임만 필요하므로 컨텍스트를 미리 닫아 자원 절약
avformat_close_input(&fmtCtx_);
fmtCtx_ = nullptr;
}
LOGI("Successfully loaded media (Type: %s, W: %d, H: %d)",
(type_ == Type::IMAGE ? "Image" : "Video"), width_, height_);
}
MediaAsset::~MediaAsset() {
release();
}
void MediaAsset::release() {
imageData_ = nullptr; // rgbBuffer_가 해제될 것이므로 포인터만 초기화
if (packet_) av_packet_free(&packet_);
if (frame_) av_frame_free(&frame_);
if (codecCtx_) avcodec_free_context(&codecCtx_);
if (fmtCtx_) avformat_close_input(&fmtCtx_);
if (swsCtx_) sws_freeContext(swsCtx_);
packet_ = nullptr;
frame_ = nullptr;
codecCtx_ = nullptr;
fmtCtx_ = nullptr;
swsCtx_ = nullptr;
type_ = Type::UNKNOWN;
width_ = 0;
height_ = 0;
videoStreamIdx_ = -1;
rgbBuffer_.clear();
}
bool MediaAsset::isValid() const {
if (type_ == Type::IMAGE) {
return !rgbBuffer_.empty() && width_ > 0 && height_ > 0;
} else if (type_ == Type::VIDEO) {
return fmtCtx_ != nullptr && codecCtx_ != nullptr && frame_ != nullptr && swsCtx_ != nullptr;
}
return false;
}
// --- Move Constructor and Assignment Operator ---
MediaAsset::MediaAsset(MediaAsset&& other) noexcept
: type_(other.type_), width_(other.width_), height_(other.height_),
imageData_(nullptr), fmtCtx_(other.fmtCtx_), codecCtx_(other.codecCtx_),
frame_(other.frame_), packet_(other.packet_), swsCtx_(other.swsCtx_),
videoStreamIdx_(other.videoStreamIdx_), rgbBuffer_(std::move(other.rgbBuffer_)) {
if (!rgbBuffer_.empty()) {
imageData_ = rgbBuffer_.data();
}
other.imageData_ = nullptr; other.fmtCtx_ = nullptr; other.codecCtx_ = nullptr;
other.frame_ = nullptr; other.packet_ = nullptr; other.swsCtx_ = nullptr;
other.type_ = Type::UNKNOWN; other.videoStreamIdx_ = -1;
}
MediaAsset& MediaAsset::operator=(MediaAsset&& other) noexcept {
if (this != &other) {
release();
type_ = other.type_; width_ = other.width_; height_ = other.height_;
fmtCtx_ = other.fmtCtx_; codecCtx_ = other.codecCtx_; frame_ = other.frame_;
packet_ = other.packet_; swsCtx_ = other.swsCtx_; videoStreamIdx_ = other.videoStreamIdx_;
rgbBuffer_ = std::move(other.rgbBuffer_);
if (!rgbBuffer_.empty()) {
imageData_ = rgbBuffer_.data();
}
other.imageData_ = nullptr; other.fmtCtx_ = nullptr; other.codecCtx_ = nullptr;
other.frame_ = nullptr; other.packet_ = nullptr; other.swsCtx_ = nullptr;
other.type_ = Type::UNKNOWN; other.videoStreamIdx_ = -1;
}
return *this;
}
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <fstream>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}
class MediaAsset {
public:
enum class Type { UNKNOWN, IMAGE, VIDEO };
MediaAsset() = default;
~MediaAsset();
MediaAsset(MediaAsset&& other) noexcept;
MediaAsset& operator=(MediaAsset&& other) noexcept;
bool load(int fd); // 수정
// bool load(const std::string& path);
void release();
bool isValid() const;
Type getType() const { return type_; }
int getWidth() const { return width_; }
int getHeight() const { return height_; }
uint8_t* getImageData() const { return imageData_; }
AVFormatContext* getFormatContext() const { return fmtCtx_; }
AVCodecContext* getCodecContext() const { return codecCtx_; }
AVFrame* getFrame() const { return frame_; }
AVPacket* getPacket() const { return packet_; }
SwsContext* getSwsContext() const { return swsCtx_; }
int getVideoStreamIndex() const { return videoStreamIdx_; }
std::vector<uint8_t>& getRgbBuffer() { return rgbBuffer_; }
private:
void loadVideo(const std::string& path);
void loadImage(const std::string& path);
void loadMediaWithFFmpeg(const std::string& path);
Type type_ = Type::UNKNOWN;
int width_ = 0;
int height_ = 0;
uint8_t* imageData_ = nullptr;
AVFormatContext* fmtCtx_ = nullptr;
AVCodecContext* codecCtx_ = nullptr;
AVFrame* frame_ = nullptr;
AVPacket* packet_ = nullptr;
SwsContext* swsCtx_ = nullptr;
int videoStreamIdx_ = -1;
std::vector<uint8_t> rgbBuffer_;
};
+78
View File
@@ -0,0 +1,78 @@
#include "Preloader.h"
#include <android/log.h>
extern void callNextMediaCallback();
#define LOG_TAG "Preloader"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
Preloader::Preloader() : dataReady_(false), preloadFailed_(false) {}
Preloader::~Preloader() {
releasePreloadedData();
}
void Preloader::releasePreloadedData() {
std::lock_guard<std::mutex> lock(preloadMutex_);
nextMedia_.release();
dataReady_ = false;
preloadFailed_ = false;
}
// 수정됨: std::string path 대신 int fd를 인자로 받음
void Preloader::startNextPreload(int fd) {
if (dataReady_ || preloadFailed_) {
return;
}
if (fd < 0) {
LOGE("Preloader received an invalid file descriptor.");
return;
}
preloadFailed_ = false;
// 백그라운드 스레드에서 로딩 작업 수행
std::thread([this, fd]() {
LOGI("Starting preload thread for media with fd: %d", fd);
std::lock_guard<std::mutex> lock(preloadMutex_);
// MediaAsset의 load(fd) 함수 호출
if (nextMedia_.load(fd)) {
dataReady_ = true;
LOGI("Preloading finished successfully.");
} else {
LOGE("Preloading failed.");
preloadFailed_ = true;
nextMedia_.release();
// 로딩 실패 시 다음 미디어를 요청하여 멈추지 않도록 함
callNextMediaCallback();
}
}).detach();
}
bool Preloader::isPreloadedDataReady() const {
return dataReady_;
}
bool Preloader::hasPreloadFailed() const {
return preloadFailed_;
}
MediaAsset Preloader::swapAndRelease() {
std::lock_guard<std::mutex> lock(preloadMutex_);
if (dataReady_) {
dataReady_ = false;
preloadFailed_ = false;
return std::move(nextMedia_);
}
return MediaAsset(); // 준비되지 않았으면 빈 MediaAsset 반환
}
void Preloader::setPreloadedMedia(MediaAsset&& media) {
std::lock_guard<std::mutex> lock(preloadMutex_);
nextMedia_ = std::move(media);
dataReady_ = true;
preloadFailed_ = false;
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "MediaAsset.h"
#include <string>
#include <mutex>
#include <atomic>
#include <thread>
class Preloader {
public:
Preloader();
~Preloader();
void startNextPreload(int fd);
bool isPreloadedDataReady() const;
bool hasPreloadFailed() const;
MediaAsset swapAndRelease();
void setPreloadedMedia(MediaAsset&& media);
void releasePreloadedData();
private:
std::mutex preloadMutex_;
MediaAsset nextMedia_;
std::atomic<bool> dataReady_;
std::atomic<bool> preloadFailed_;
};
+234
View File
@@ -0,0 +1,234 @@
#include "Renderer.h"
#include "Preloader.h"
#include <android/log.h>
#include <algorithm>
#include <cmath>
#include <thread>
extern Preloader* preloader; // 전역 Preloader 객체
extern void callNextMediaCallback(); // 다음 미디어 요청 콜백
#define LOG_TAG "Renderer"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
static constexpr long long displayDurationMs = 20000;
static constexpr long long fadeDurationMs = 3000;
Renderer::Renderer() {
mediaStartTime_ = std::chrono::steady_clock::now();
}
Renderer::~Renderer() {
release();
}
void Renderer::release() {
std::lock_guard<std::mutex> lock(renderMutex_);
currentMedia_.release();
nextMedia_.release();
}
void Renderer::setNextMedia(int fd) {
std::lock_guard<std::mutex> lock(renderMutex_);
currentMedia_.release();
if (currentMedia_.load(fd)) {
LOGI("New media loaded successfully.");
mediaStartTime_ = std::chrono::steady_clock::now();
isFading_ = false;
} else {
LOGE("Failed to load new media: %s. Requesting next media.", currentMediaPath_.c_str());
// NOTE: 초기 로딩 실패 시, 다음 미디어를 바로 요청하여 검은 화면 방지
callNextMediaCallback();
}
}
void Renderer::renderFrame(ANativeWindow* window) {
if (!window) return;
std::lock_guard<std::mutex> lock(renderMutex_);
if (!currentMedia_.isValid()) {
// NOTE: 렌더링할 미디어가 없으면 아무것도 하지 않음 (검은 화면)
// 로딩 실패 시 setNextMedia에서 다음 미디어를 요청하므로 일시적인 상태
return;
}
ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(window, &buffer, nullptr) < 0) {
LOGE("Failed to lock window.");
return;
}
// 화면을 검은색으로 초기화
memset(buffer.bits, 0, buffer.stride * buffer.height * sizeof(uint32_t));
auto now = std::chrono::steady_clock::now();
long long elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(now - mediaStartTime_).count();
// 1. 현재 미디어 그리기 (페이드 아웃 효과 적용)
float currentAlpha = 1.0f;
if (isFading_) {
long long fadeElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - fadeStartTime_).count();
currentAlpha = std::clamp(1.0f - (float)fadeElapsed / fadeDurationMs, 0.0f, 1.0f);
}
drawMedia(buffer, currentMedia_, currentAlpha, elapsedMs);
// 2. 다음 미디어 그리기 (페이드 인 효과 적용)
if (isFading_) {
// Preloader로부터 미리 로드된 미디어를 가져오기 시도
if (!nextMedia_.isValid()) {
if (preloader && preloader->isPreloadedDataReady()) {
nextMedia_ = preloader->swapAndRelease();
if (nextMedia_.isValid()) {
LOGI("Renderer acquired preloaded media for fade-in.");
// 성공적으로 가져왔으면, 다음 미디어 예비 로딩 요청
callNextMediaCallback();
} else {
LOGE("Renderer failed to acquire a valid preloaded media.");
}
}
}
// 가져온 nextMedia_가 유효하다면 페이드 인 효과로 그리기
if (nextMedia_.isValid()) {
long long fadeElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - fadeStartTime_).count();
float nextAlpha = std::clamp((float)fadeElapsed / fadeDurationMs, 0.0f, 1.0f);
drawMedia(buffer, nextMedia_, nextAlpha, 0); // 새 미디어는 0ms부터 시작
}
}
// 3. 미디어 상태 업데이트
long long fadeTotalElapsed = isFading_ ? std::chrono::duration_cast<std::chrono::milliseconds>(now - fadeStartTime_).count() : 0;
if (isFading_ && fadeTotalElapsed >= fadeDurationMs) {
// 페이드가 끝났을 때
if (nextMedia_.isValid()) {
// NOTE: 다음 미디어가 유효할 때만 현재 미디어를 교체 (가장 중요)
currentMedia_ = std::move(nextMedia_);
LOGI("Renderer successfully swapped to new media.");
} else {
LOGE("Fade ended, but no valid next media. Retaining current media.");
}
isFading_ = false;
mediaStartTime_ = now;
} else if (!isFading_ && elapsedMs >= displayDurationMs) {
// 현재 미디어 재생 시간이 다 되어 페이드를 시작할 때
isFading_ = true;
fadeStartTime_ = now;
LOGI("Display duration ended. Starting fade transition.");
}
ANativeWindow_unlockAndPost(window);
}
// --- 나머지 함수들은 변경 사항 없음 ---
void Renderer::drawMedia(ANativeWindow_Buffer& buffer, const MediaAsset& media, float alpha, float offsetElapsedMs) {
if (!media.isValid() || alpha <= 0.0f) return;
float mediaW = static_cast<float>(media.getWidth());
float mediaH = static_cast<float>(media.getHeight());
float bufW = static_cast<float>(buffer.width);
float bufH = static_cast<float>(buffer.height);
float scale, overflowX, overflowY;
if ((mediaW / mediaH) > (bufW / bufH)) {
scale = bufH / mediaH;
overflowX = std::max(0.0f, mediaW * scale - bufW);
overflowY = 0.0f;
} else {
scale = bufW / mediaW;
overflowX = 0.0f;
overflowY = std::max(0.0f, mediaH * scale - bufH);
}
float offsetX = 0.0f;
float offsetY = 0.0f;
updateOffset(offsetX, offsetY, overflowX, overflowY, offsetElapsedMs);
if (media.getType() == MediaAsset::Type::IMAGE) {
renderImageFrame(media, buffer, scale, offsetX, offsetY, alpha);
} else {
renderVideoFrame(media, buffer, scale, offsetX, offsetY, alpha);
}
}
void Renderer::updateOffset(float& offsetX, float& offsetY, float overflowX, float overflowY, long long elapsedMs) {
if (overflowX > 0) {
float normalizedTime = fmod((float)elapsedMs / displayDurationMs, 2.0f);
normalizedTime = (normalizedTime > 1.0f) ? 2.0f - normalizedTime : normalizedTime;
offsetX = overflowX * normalizedTime;
}
if (overflowY > 0) {
float normalizedTime = fmod((float)elapsedMs / displayDurationMs, 2.0f);
normalizedTime = (normalizedTime > 1.0f) ? 2.0f - normalizedTime : normalizedTime;
offsetY = overflowY * normalizedTime;
}
}
void Renderer::renderImageFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha) {
uint32_t* dstPixels = (uint32_t*)buffer.bits;
int dstStride = buffer.stride;
const uint8_t* pixelData = media.getImageData();
int imgW = media.getWidth();
int imgH = media.getHeight();
uint8_t alphaByte = static_cast<uint8_t>(alpha * 255.0f);
for (int y = 0; y < buffer.height; ++y) {
int srcY = static_cast<int>((y + offsetY) / scale);
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);
if (srcX < 0 || srcX >= imgW) continue;
const uint8_t* srcPixel = &pixelData[(srcY * imgW + srcX) * 4];
uint32_t dstPixel = dstRow[x];
uint8_t dstR = (dstPixel >> 16) & 0xFF;
uint8_t dstG = (dstPixel >> 8) & 0xFF;
uint8_t dstB = dstPixel & 0xFF;
uint8_t finalR = (srcPixel[0] * alphaByte + dstR * (255 - alphaByte)) / 255;
uint8_t finalG = (srcPixel[1] * alphaByte + dstG * (255 - alphaByte)) / 255;
uint8_t finalB = (srcPixel[2] * alphaByte + dstB * (255 - alphaByte)) / 255;
dstRow[x] = (0xFF << 24) | (finalR << 16) | (finalG << 8) | finalB;
}
}
}
void Renderer::renderVideoFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha) {
AVFormatContext* fmt_ctx = media.getFormatContext();
AVCodecContext* codec_ctx = media.getCodecContext();
AVFrame* frame = media.getFrame();
AVPacket* pkt = media.getPacket();
SwsContext* sws_ctx = media.getSwsContext();
int video_stream_idx = media.getVideoStreamIndex();
if (!fmt_ctx || !codec_ctx || !frame || !pkt || !sws_ctx) return;
std::vector<uint8_t>& rgbBuf = const_cast<MediaAsset&>(media).getRgbBuffer();
int ret = av_read_frame(fmt_ctx, pkt);
if (ret >= 0) {
if (pkt->stream_index == video_stream_idx) {
if (avcodec_send_packet(codec_ctx, pkt) >= 0) {
if (avcodec_receive_frame(codec_ctx, frame) == 0) {
uint8_t* dst[4] = { rgbBuf.data(), nullptr, nullptr, nullptr };
int dstStride_arr[4] = { media.getWidth() * 4, 0, 0, 0 };
sws_scale(sws_ctx, frame->data, frame->linesize, 0, media.getHeight(), dst, dstStride_arr);
}
}
}
av_packet_unref(pkt);
} else if (ret == AVERROR_EOF) {
av_seek_frame(fmt_ctx, video_stream_idx, 0, AVSEEK_FLAG_BACKWARD);
}
// 디코딩된 프레임이 버퍼에 있으므로, renderImageFrame과 유사한 로직으로 화면에 그립니다.
renderImageFrame(media, buffer, scale, offsetX, offsetY, alpha);
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include "MediaAsset.h"
#include <chrono>
#include <string>
#include <vector>
#include <mutex>
#include <android/native_window.h>
#include <android/native_window_jni.h>
class Renderer {
public:
Renderer();
~Renderer();
void setNextMedia(int fd);
void renderFrame(ANativeWindow* window);
void release();
private:
std::mutex renderMutex_;
MediaAsset currentMedia_;
MediaAsset nextMedia_;
std::string currentMediaPath_;
std::chrono::steady_clock::time_point mediaStartTime_;
std::chrono::steady_clock::time_point fadeStartTime_;
bool isFading_ = false;
void drawMedia(ANativeWindow_Buffer& buffer, const MediaAsset& media, float alpha, float offsetElapsedMs);
void updateOffset(float& offsetX, float& offsetY, float overflowX, float overflowY, long long elapsedMs);
void renderVideoFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha);
void renderImageFrame(const MediaAsset& media, ANativeWindow_Buffer& buffer, float scale, float offsetX, float offsetY, float alpha);
};
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include <libavfilter/avfilter.h>
#ifdef __cplusplus
}
#endif
+87 -670
View File
@@ -1,697 +1,114 @@
#include <jni.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <android/log.h>
#include <jni.h>
#include <mutex>
#include <vector>
#include <string>
#include <chrono>
#include <thread>
#include <algorithm>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include "Renderer.h"
#include "Preloader.h"
#define LOG_TAG "NativeRenderer"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
static ANativeWindow* window = nullptr;
// 전역 변수
Renderer* renderer = nullptr;
Preloader* preloader = nullptr;
JavaVM* g_vm = nullptr;
jobject g_callback_obj = nullptr;
jmethodID g_callback_method_id = nullptr;
// 비디오 변
static AVFormatContext* fmt_ctx = nullptr;
static AVCodecContext* codec_ctx = nullptr;
static AVFrame* frame = nullptr;
static AVPacket* pkt = nullptr;
static SwsContext* sws_ctx = nullptr;
static int video_stream_idx = -1;
static int videoWidth = 0;
static int videoHeight = 0;
static std::vector<uint8_t> rgbBuffer;
// JNI_OnLoad 함
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
g_vm = vm;
return JNI_VERSION_1_6;
}
// 이미지 변수
static uint8_t* imageData = nullptr;
static int imageWidth = 0;
static int imageHeight = 0;
static int imageChannels = 0;
static bool isImage = false;
void callNextMediaCallback() {
JNIEnv* env;
bool isAttached = false;
// 현재 스레드가 JVM에 연결되어 있지 않다면 연결
if (g_vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
g_vm->AttachCurrentThread(&env, nullptr);
isAttached = true;
}
// 다음 미디어 변수
static AVFormatContext* next_fmt_ctx = nullptr;
static AVCodecContext* next_codec_ctx = nullptr;
static AVFrame* next_frame = nullptr;
static AVPacket* next_pkt = nullptr;
static SwsContext* next_sws_ctx = nullptr;
static int next_video_stream_idx = -1;
static int next_videoWidth = 0;
static int next_videoHeight = 0;
static std::vector<uint8_t> next_rgbBuffer;
if (g_callback_obj && g_callback_method_id) {
env->CallVoidMethod(g_callback_obj, g_callback_method_id);
}
static uint8_t* next_imageData = nullptr;
static int next_imageWidth = 0;
static int next_imageHeight = 0;
static int next_imageChannels = 0;
static bool nextIsImage = false;
static bool nextMediaReady = false;
static std::mutex renderMutex;
static constexpr float frameDurationMs = 16.0f;
static constexpr long long displayDurationMs = 20000;
static constexpr long long fadeDurationMs = 3000;
static std::vector<std::string> mediaPaths;
static int currentMediaIndex = 0;
static int nextMediaIndex = 1;
// 애니메이션 변수
static float offsetX = 0.f;
static float offsetY = 0.f;
static bool movingForwardLocX = true;
static bool movingDownLocY = true;
// 페이드 및 미디어 전환 시간 상태
static std::chrono::steady_clock::time_point mediaStartTime;
static std::chrono::steady_clock::time_point fadeStartTime;
static bool isFading = false;
// 페이드 알파값
static float fadeOutAlpha = 1.f;
static float fadeInAlpha = 0.f;
// ==================== 메모리 해제: 이미지 ====================
static void releaseImageData(uint8_t** data) {
if (*data) {
stbi_image_free(*data);
*data = nullptr;
// 이전에 연결되지 않았다면 연결 해제
if (isAttached) {
g_vm->DetachCurrentThread();
}
}
// ==================== 메모리 해제: FFmpeg 컨텍스트 ====================
static void releaseFFmpegContext(
AVFormatContext** fctx, AVCodecContext** cctx,
AVFrame** frm, AVPacket** pck,
SwsContext** sws, std::vector<uint8_t>* buffer) {
if (*cctx) avcodec_free_context(cctx);
if (*fctx) avformat_close_input(fctx);
if (*frm) av_frame_free(frm);
if (*pck) av_packet_free(pck);
if (*sws) sws_freeContext(*sws);
if (buffer) buffer->clear();
*cctx = nullptr;
*fctx = nullptr;
*frm = nullptr;
*pck = nullptr;
*sws = nullptr;
}
// ==================== 미디어 데이터 해제 ====================
static void releaseMediaData(bool loadIsImage,
uint8_t** imgData,
AVFormatContext** fmtCtx, AVCodecContext** codecCtx,
AVFrame** frm, AVPacket** pck, SwsContext** sws,
std::vector<uint8_t>* rgbBuf) {
if (loadIsImage) {
releaseImageData(imgData);
} else {
releaseFFmpegContext(fmtCtx, codecCtx, frm, pck, sws, rgbBuf);
}
}
// ==================== 스케일 계산 구조체 및 함수 ====================
struct ScaleResult {
float scale;
float scaledW;
float scaledH;
float overflowX;
float overflowY;
};
static ScaleResult calculateScale(float mediaW, float mediaH, float bufW, float bufH) {
ScaleResult res{};
if ((mediaW / mediaH) > (bufW / bufH)) {
res.scale = bufH / mediaH;
res.scaledW = mediaW * res.scale;
res.scaledH = bufH;
} else {
res.scale = bufW / mediaW;
res.scaledW = bufW;
res.scaledH = mediaH * res.scale;
}
res.overflowX = std::max(0.f, res.scaledW - bufW);
res.overflowY = std::max(0.f, res.scaledH - bufH);
return res;
}
// ==================== 오프셋 애니메이션 업데이트 ====================
static void updateOffset(float& offsetX, float& offsetY,
bool& movingX, bool& movingY,
float overflowX, float overflowY) {
if (overflowX > 0) {
float speedX = overflowX / displayDurationMs;
float deltaX = speedX * frameDurationMs;
if (movingX) {
offsetX += deltaX;
if (offsetX >= overflowX) {
offsetX = overflowX;
movingX = false;
}
} else {
offsetX -= deltaX;
if (offsetX <= 0) {
offsetX = 0.f;
movingX = true;
}
}
} else {
offsetX = 0.f;
}
if (overflowY > 0) {
float speedY = overflowY / displayDurationMs;
float deltaY = speedY * frameDurationMs;
if (movingY) {
offsetY += deltaY;
if (offsetY >= overflowY) {
offsetY = overflowY;
movingY = false;
}
} else {
offsetY -= deltaY;
if (offsetY <= 0) {
offsetY = 0.f;
movingY = true;
}
}
} else {
offsetY = 0.f;
}
}
// ==================== 버퍼 클리어 함수 ====================
static void clearBufferIfNeeded(ANativeWindow_Buffer& buffer, bool shouldClear) {
if (!shouldClear) return;
uint32_t* dstPixels = (uint32_t*)buffer.bits;
int dstStride = buffer.stride;
for (int y = 0; y < buffer.height; ++y) {
uint32_t* dstRow = dstPixels + y * dstStride;
for (int x = 0; x < buffer.width; ++x) {
dstRow[x] = 0x00000000; // 완전 투명 또는 검은색
}
}
}
// ==================== 픽셀 그리기: 이미지 및 비디오 프레임 ====================
static void drawToBuffer(ANativeWindow_Buffer& buffer,
uint8_t* pixelData, int imgW, int imgH,
float scale, float offsetX, float offsetY, float alpha) {
if (alpha <= 0.f) return;
alpha = std::clamp(alpha, 0.f, 1.f);
uint32_t* dstPixels = (uint32_t*)buffer.bits;
int dstStride = buffer.stride;
for (int y = 0; y < buffer.height; ++y) {
int srcY = (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 = (int)((x + offsetX) / scale);
if (srcX < 0 || srcX >= imgW) continue;
uint8_t* px = &pixelData[(srcY * imgW + srcX) * 4];
uint8_t r = (uint8_t)(px[0] * alpha);
uint8_t g = (uint8_t)(px[1] * alpha);
uint8_t b = (uint8_t)(px[2] * alpha);
uint8_t a = (uint8_t)(px[3] * alpha);
dstRow[x] = (a << 24) | (r << 16) | (g << 8) | b;
}
}
}
// ==================== 미디어 로딩 함수(이미지/비디오) ====================
static bool loadMedia(const std::string& path, bool loadIsImage,
uint8_t** imgData, int* imgW, int* imgH, int* imgCh,
AVFormatContext** fmtCtx, AVCodecContext** codecCtx, AVFrame** frm, AVPacket** pck,
SwsContext** sws, int* videoIdx, int* vidW, int* vidH,
std::vector<uint8_t>* rgbBuf) {
try {
if (!loadIsImage) {
*fmtCtx = avformat_alloc_context();
if (avformat_open_input(fmtCtx, path.c_str(), nullptr, nullptr) != 0) {
LOGE("Failed to open video: %s", path.c_str());
return false;
}
if (avformat_find_stream_info(*fmtCtx, nullptr) < 0) {
LOGE("Failed to get stream info: %s", path.c_str());
avformat_close_input(fmtCtx);
return false;
}
*videoIdx = -1;
for (unsigned int i = 0; i < (*fmtCtx)->nb_streams; ++i) {
if ((*fmtCtx)->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
*videoIdx = i;
break;
}
}
if (*videoIdx == -1) {
LOGE("No video stream found: %s", path.c_str());
avformat_close_input(fmtCtx);
return false;
}
AVCodecParameters* codecpar = (*fmtCtx)->streams[*videoIdx]->codecpar;
const AVCodec* codec = avcodec_find_decoder(codecpar->codec_id);
if (!codec) {
LOGE("Decoder not found");
avformat_close_input(fmtCtx);
return false;
}
*codecCtx = avcodec_alloc_context3(codec);
if (!*codecCtx) {
LOGE("Failed to alloc codec context");
avformat_close_input(fmtCtx);
return false;
}
if (avcodec_parameters_to_context(*codecCtx, codecpar) < 0) {
LOGE("Failed to copy codec params");
avcodec_free_context(codecCtx);
avformat_close_input(fmtCtx);
return false;
}
if (avcodec_open2(*codecCtx, codec, nullptr) < 0) {
LOGE("Failed to open codec");
avcodec_free_context(codecCtx);
avformat_close_input(fmtCtx);
return false;
}
*vidW = (*codecCtx)->width;
*vidH = (*codecCtx)->height;
*frm = av_frame_alloc();
*pck = av_packet_alloc();
*sws = sws_getContext(*vidW, *vidH, (*codecCtx)->pix_fmt, *vidW, *vidH,
AV_PIX_FMT_RGBA, SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!*sws) {
LOGE("Failed to create sws context");
av_frame_free(frm);
av_packet_free(pck);
avcodec_free_context(codecCtx);
avformat_close_input(fmtCtx);
return false;
}
rgbBuf->resize((*vidW) * (*vidH) * 4);
*imgData = nullptr;
*imgW = 0;
*imgH = 0;
*imgCh = 0;
} else {
*imgData = stbi_load(path.c_str(), imgW, imgH, imgCh, 4);
if (!*imgData) {
LOGE("Failed to load image: %s", path.c_str());
return false;
}
*fmtCtx = nullptr;
*codecCtx = nullptr;
*frm = nullptr;
*pck = nullptr;
*sws = nullptr;
*videoIdx = -1;
*vidW = 0;
*vidH = 0;
rgbBuf->clear();
}
LOGI("Successfully loaded media: %s", path.c_str());
return true;
} catch (...) {
LOGE("Exception occurred during media loading: %s", path.c_str());
return false;
}
}
// ==================== 다음 미디어 비동기 로드 ====================
static bool loadNextMedia() {
LOGI("loadNextMedia: Trying to load media index %d", nextMediaIndex);
releaseMediaData(nextIsImage, &next_imageData, &next_fmt_ctx, &next_codec_ctx,
&next_frame, &next_pkt, &next_sws_ctx, &next_rgbBuffer);
if (mediaPaths.empty()) {
LOGE("loadNextMedia: mediaPaths is empty");
return false;
}
const std::string& nextPath = mediaPaths[nextMediaIndex];
LOGI("loadNextMedia: nextPath=%s", nextPath.c_str());
nextIsImage = (nextPath.find(".mp4") == std::string::npos &&
nextPath.find(".mkv") == std::string::npos);
bool ok = loadMedia(nextPath, nextIsImage,
&next_imageData, &next_imageWidth, &next_imageHeight, &next_imageChannels,
&next_fmt_ctx, &next_codec_ctx, &next_frame, &next_pkt, &next_sws_ctx,
&next_video_stream_idx, &next_videoWidth, &next_videoHeight, &next_rgbBuffer);
if (!ok) {
LOGE("loadNextMedia: Failed to load media %s", nextPath.c_str());
} else {
LOGI("loadNextMedia: Successfully loaded media");
}
return ok;
}
// ==================== 비디오/이미지 렌더링 ====================
static void renderMedia(ANativeWindow_Buffer& buffer,
uint8_t* imgData, int imgW, int imgH, int imgCh,
AVFormatContext* fctx, AVCodecContext* cctx,
AVFrame* frm, AVPacket* pck, SwsContext* sws,
int vidStreamIdx, int vidW, int vidH,
std::vector<uint8_t>& rgbBuf,
bool isImageLocal,
float scale,
float offsetXLocal,
float offsetYLocal,
float alpha) {
if (isImageLocal) {
drawToBuffer(buffer, imgData, imgW, imgH, scale, offsetXLocal, offsetYLocal, alpha);
return;
}
if (!fctx || !cctx) return;
int ret = av_read_frame(fctx, pck);
bool gotFrame = false;
while (ret >= 0) {
if (pck->stream_index == vidStreamIdx) {
ret = avcodec_send_packet(cctx, pck);
if (ret < 0) break;
ret = avcodec_receive_frame(cctx, frm);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
av_packet_unref(pck);
ret = av_read_frame(fctx, pck);
continue;
} else if (ret < 0) break;
uint8_t* dst[4] = { rgbBuf.data(), nullptr, nullptr, nullptr };
int dstStride_arr[4] = { vidW * 4, 0, 0, 0 };
sws_scale(sws, frm->data, frm->linesize, 0, vidH, dst, dstStride_arr);
gotFrame = true;
break;
}
av_packet_unref(pck);
ret = av_read_frame(fctx, pck);
}
av_packet_unref(pck);
if (!gotFrame) {
av_seek_frame(fctx, vidStreamIdx, 0, AVSEEK_FLAG_BACKWARD);
return;
}
drawToBuffer(buffer, rgbBuf.data(), vidW, vidH, scale, offsetXLocal, offsetYLocal, alpha);
}
// ==================== 페이드 인/아웃 크로스렌더링 ====================
static void renderWithFade(ANativeWindow_Buffer& buffer,
float bufW, float bufH,
uint8_t* curImgData, int curImgW, int curImgH, int curImgCh,
AVFormatContext* curFmtCtx, AVCodecContext* curCodecCtx,
AVFrame* curFrame, AVPacket* curPkt, SwsContext* curSwsCtx,
int curVidStreamIdx, int curVidW, int curVidH,
std::vector<uint8_t>& curRgbBuf,
bool curIsImage,
uint8_t* nextImgData, int nextImgW, int nextImgH, int nextImgCh,
AVFormatContext* nextFmtCtx, AVCodecContext* nextCodecCtx,
AVFrame* nextFrame, AVPacket* nextPkt, SwsContext* nextSwsCtx,
int nextVidStreamIdx, int nextVidW, int nextVidH,
std::vector<uint8_t>& nextRgbBuf,
bool nextIsImage,
float fadeOutAlpha, float fadeInAlpha,
float& curOffsetX, float& curOffsetY,
bool& curMovingX, bool& curMovingY,
float& nextOffsetX, float& nextOffsetY,
bool& nextMovingX, bool& nextMovingY) {
auto curScaleRes = calculateScale(
curIsImage ? (float)curImgW : (float)curVidW,
curIsImage ? (float)curImgH : (float)curVidH,
bufW, bufH);
auto nextScaleRes = calculateScale(
nextIsImage ? (float)nextImgW : (float)nextVidW,
nextIsImage ? (float)nextImgH : (float)nextVidH,
bufW, bufH);
updateOffset(curOffsetX, curOffsetY, curMovingX, curMovingY, curScaleRes.overflowX, curScaleRes.overflowY);
updateOffset(nextOffsetX, nextOffsetY, nextMovingX, nextMovingY, nextScaleRes.overflowX, nextScaleRes.overflowY);
if (curIsImage) {
drawToBuffer(buffer, curImgData, curImgW, curImgH,
curScaleRes.scale, curOffsetX, curOffsetY, fadeOutAlpha);
} else {
renderMedia(buffer, curImgData, curImgW, curImgH, curImgCh,
curFmtCtx, curCodecCtx, curFrame, curPkt, curSwsCtx,
curVidStreamIdx, curVidW, curVidH, curRgbBuf,
false,
curScaleRes.scale, curOffsetX, curOffsetY,
fadeOutAlpha);
}
if (nextIsImage) {
drawToBuffer(buffer, nextImgData, nextImgW, nextImgH,
nextScaleRes.scale, nextOffsetX, nextOffsetY, fadeInAlpha);
} else {
renderMedia(buffer, nextImgData, nextImgW, nextImgH, nextImgCh,
nextFmtCtx, nextCodecCtx, nextFrame, nextPkt, nextSwsCtx,
nextVidStreamIdx, nextVidW, nextVidH, nextRgbBuf,
false,
nextScaleRes.scale, nextOffsetX, nextOffsetY,
fadeInAlpha);
}
}
// ==================== JNI 함수: 미디어 리스트 세팅 ====================
extern "C" {
JNIEXPORT void JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetMediaList(JNIEnv* env, jobject, jobjectArray paths) {
std::lock_guard<std::mutex> lock(renderMutex);
mediaPaths.clear();
jsize len = env->GetArrayLength(paths);
for (jsize i = 0; i < len; ++i) {
jstring pathStr = (jstring) env->GetObjectArrayElement(paths, i);
const char* pathCStr = env->GetStringUTFChars(pathStr, nullptr);
mediaPaths.push_back(std::string(pathCStr));
env->ReleaseStringUTFChars(pathStr, pathCStr);
env->DeleteLocalRef(pathStr);
}
currentMediaIndex = 0;
nextMediaIndex = (len > 1) ? 1 : 0;
releaseMediaData(isImage, &imageData, &fmt_ctx, &codec_ctx, &frame, &pkt, &sws_ctx, &rgbBuffer);
isImage = false;
if (!mediaPaths.empty()) {
const bool loadIsImage = (mediaPaths[0].find(".mp4") == std::string::npos &&
mediaPaths[0].find(".mkv") == std::string::npos);
isImage = loadIsImage;
if (!loadMedia(mediaPaths[0], loadIsImage,
&imageData, &imageWidth, &imageHeight, &imageChannels,
&fmt_ctx, &codec_ctx, &frame, &pkt, &sws_ctx,
&video_stream_idx, &videoWidth, &videoHeight, &rgbBuffer)) {
LOGE("Failed to load the first media");
return;
}
offsetX = 0.f; offsetY = 0.f;
movingForwardLocX = true; movingDownLocY = true;
mediaStartTime = std::chrono::steady_clock::now();
isFading = false;
nextMediaReady = false;
releaseMediaData(nextIsImage, &next_imageData, &next_fmt_ctx, &next_codec_ctx,
&next_frame, &next_pkt, &next_sws_ctx, &next_rgbBuffer);
std::thread([](){
std::lock_guard<std::mutex> preloadLock(renderMutex);
nextMediaReady = loadNextMedia();
if (nextMediaReady) {
LOGI("Preloaded next media ready");
} else {
LOGE("Preload failed");
}
}).detach();
}
}
JNIEXPORT void JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeRender(JNIEnv* env, jobject) {
std::lock_guard<std::mutex> lock(renderMutex);
if (!window || mediaPaths.empty()) {
LOGI("nativeRender: no window or empty media");
return;
}
ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(window, &buffer, nullptr) < 0) {
LOGE("nativeRender: Failed to lock window");
return;
}
clearBufferIfNeeded(buffer, !isFading);
auto now = std::chrono::steady_clock::now();
auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(now - mediaStartTime).count();
elapsedMs = std::max(elapsedMs, 0LL);
if (!isFading && elapsedMs > (displayDurationMs - fadeDurationMs - 10) && nextMediaReady) {
isFading = true;
fadeStartTime = now;
LOGI("Fade started");
}
if (isFading) {
auto fadeElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - fadeStartTime).count();
fadeOutAlpha = std::clamp(1.f - (float)fadeElapsed / fadeDurationMs, 0.f, 1.f);
fadeInAlpha = std::clamp((float)fadeElapsed / fadeDurationMs, 0.f, 1.f);
static float nextOffsetX = 0.f;
static float nextOffsetY = 0.f;
static bool nextMovingForwardX = true;
static bool nextMovingDownY = true;
renderWithFade(buffer,
(float)buffer.width, (float)buffer.height,
imageData, imageWidth, imageHeight, imageChannels,
fmt_ctx, codec_ctx, frame, pkt, sws_ctx,
video_stream_idx, videoWidth, videoHeight,
rgbBuffer,
isImage,
next_imageData, next_imageWidth, next_imageHeight, next_imageChannels,
next_fmt_ctx, next_codec_ctx, next_frame, next_pkt, next_sws_ctx,
next_video_stream_idx, next_videoWidth, next_videoHeight,
next_rgbBuffer,
nextIsImage,
fadeOutAlpha, fadeInAlpha,
offsetX, offsetY,
movingForwardLocX, movingDownLocY,
nextOffsetX, nextOffsetY,
nextMovingForwardX, nextMovingDownY);
if (fadeElapsed >= fadeDurationMs) {
LOGI("Fade ended, switching media");
releaseMediaData(isImage, &imageData, &fmt_ctx, &codec_ctx, &frame, &pkt, &sws_ctx, &rgbBuffer);
imageData = next_imageData;
imageWidth = next_imageWidth;
imageHeight = next_imageHeight;
imageChannels = next_imageChannels;
fmt_ctx = next_fmt_ctx;
codec_ctx = next_codec_ctx;
frame = next_frame;
pkt = next_pkt;
sws_ctx = next_sws_ctx;
video_stream_idx = next_video_stream_idx;
videoWidth = next_videoWidth;
videoHeight = next_videoHeight;
rgbBuffer = std::move(next_rgbBuffer);
isImage = nextIsImage;
currentMediaIndex = nextMediaIndex;
nextMediaIndex = (nextMediaIndex + 1) % mediaPaths.size();
offsetX = 0.f; offsetY = 0.f;
movingForwardLocX = true; movingDownLocY = true;
nextOffsetX = 0.f; nextOffsetY = 0.f;
nextMovingForwardX = true; nextMovingDownY = true;
mediaStartTime = std::chrono::steady_clock::now();
isFading = false;
nextMediaReady = false;
std::thread([](){
std::lock_guard<std::mutex> preloadLock(renderMutex);
nextMediaReady = loadNextMedia();
if (nextMediaReady) {
LOGI("Preloaded next media ready");
} else {
LOGE("Preload failed");
}
}).detach();
}
} else {
auto curScaleRes = calculateScale(
isImage ? (float)imageWidth : (float)videoWidth,
isImage ? (float)imageHeight : (float)videoHeight,
(float)buffer.width, (float)buffer.height);
updateOffset(offsetX, offsetY,
movingForwardLocX, movingDownLocY,
curScaleRes.overflowX, curScaleRes.overflowY);
if (isImage) {
drawToBuffer(buffer, imageData, imageWidth, imageHeight,
curScaleRes.scale, offsetX, offsetY, 1.f);
} else {
renderMedia(buffer, imageData, imageWidth, imageHeight, imageChannels,
fmt_ctx, codec_ctx, frame, pkt, sws_ctx,
video_stream_idx, videoWidth, videoHeight, rgbBuffer,
false,
curScaleRes.scale, offsetX, offsetY,
1.f);
}
}
ANativeWindow_unlockAndPost(window);
}
JNIEXPORT jboolean JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeInit(JNIEnv* env, jobject, jobject surface) {
std::lock_guard<std::mutex> lock(renderMutex);
if (window) {
ANativeWindow_release(window);
window = nullptr;
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeInit(JNIEnv* env, jobject) {
if (!renderer) {
renderer = new Renderer();
}
window = ANativeWindow_fromSurface(env, surface);
LOGI("Native window initialized");
return true;
if (!preloader) {
preloader = new Preloader();
}
LOGI("Native renderer and preloader initialized.");
return JNI_TRUE;
}
JNIEXPORT void JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeRender(JNIEnv* env, jobject, jobject surface) {
if (!renderer || !surface) return;
ANativeWindow* window = ANativeWindow_fromSurface(env, surface);
if (!window) {
LOGE("Could not get ANativeWindow from Surface.");
return;
}
renderer->renderFrame(window);
ANativeWindow_release(window);
}
JNIEXPORT void JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeDestroy(JNIEnv* env, jobject) {
std::lock_guard<std::mutex> lock(renderMutex);
releaseMediaData(isImage, &imageData, &fmt_ctx, &codec_ctx, &frame, &pkt, &sws_ctx, &rgbBuffer);
releaseMediaData(nextIsImage, &next_imageData, &next_fmt_ctx, &next_codec_ctx, &next_frame, &next_pkt, &next_sws_ctx, &next_rgbBuffer);
if (window) {
ANativeWindow_release(window);
window = nullptr;
if (renderer) {
delete renderer;
renderer = nullptr;
}
LOGI("Native window released");
if (preloader) {
delete preloader;
preloader = nullptr;
}
if (g_callback_obj) {
env->DeleteGlobalRef(g_callback_obj);
g_callback_obj = nullptr;
}
LOGI("Native renderer destroyed and memory freed.");
}
JNIEXPORT void JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetCurrentMedia(JNIEnv* env, jobject, jint fd) {
if (!renderer) return;
// 경로 대신 파일 디스크립터를 전달
renderer->setNextMedia(fd);
}
} // extern "C"
JNIEXPORT void JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeStartNextPreload(JNIEnv* env, jobject, jint fd) {
if (!preloader) return;
// 경로 대신 파일 디스크립터를 전달
preloader->startNextPreload(fd);
}
JNIEXPORT void JNICALL
Java_bums_lunatic_launcher_wall_NativeRenderer_nativeSetNextMediaCallback(JNIEnv* env, jobject, jobject callback) {
jclass clazz = env->GetObjectClass(callback);
g_callback_method_id = env->GetMethodID(clazz, "onNextMediaRequested", "()V");
g_callback_obj = env->NewGlobalRef(callback);
if (!g_callback_method_id) {
LOGE("Could not find onNextMediaRequested method ID.");
}
}
}
@@ -52,7 +52,7 @@ internal class LunaticLauncher : Application() {
if (!dir.exists()) {
dir.mkdirs()
} else {
dir.listFiles().forEach { Blog.LOGE("child -> ${it.absolutePath}") }
// dir.listFiles().forEach { Blog.LOGE("child -> ${it.absolutePath}") }
}
mHourlyLogWriter = HourlyLogWriter(dir)
val cacheSize = 1024L * 1024 * 1024 // 60MB
@@ -384,7 +384,7 @@ class GeckoWeb : BWebview {
if (!dir.exists()) {
dir.mkdirs()
} else {
dir.listFiles().forEach { Blog.LOGE("child -> ${it.absolutePath}") }
// dir.listFiles().forEach { Blog.LOGE("child -> ${it.absolutePath}") }
}
val outputFile = File(dir, "${url?.host ?: "UnKnown"}_scraped_${SimpleDateFormat("yyyyMMdd-HHmm").format(Date())}.md")
outputFile.writeText(markdownText, Charsets.UTF_8)
@@ -127,7 +127,7 @@ object CommonUtils {
if (!dir.exists()) {
dir.mkdirs()
} else {
dir.listFiles().forEach { Blog.LOGE("child -> ${it.absolutePath}") }
// dir.listFiles().forEach { Blog.LOGE("child -> ${it.absolutePath}") }
}
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
@@ -1,10 +1,13 @@
package bums.lunatic.launcher.wall
import android.content.ContentUris
import android.os.Environment
import android.os.Handler
import android.os.HandlerThread
import android.provider.MediaStore
import android.service.wallpaper.WallpaperService
import android.view.SurfaceHolder
import bums.lunatic.launcher.utils.Blog
import java.io.File
class MyWallpaperService : WallpaperService() {
@@ -19,6 +22,7 @@ class MyWallpaperService : WallpaperService() {
private var running = false
private var nativeRenderer: NativeRenderer? = null
private var mediaFiles: List<File> = emptyList()
private var currentMediaIndex = -1
private val frameDelayMs = 16L // 약 60fps
@@ -27,12 +31,89 @@ class MyWallpaperService : WallpaperService() {
private val renderRunnable = object : Runnable {
override fun run() {
if (!running) return
nativeRenderer?.nativeRender()
if (!running || holder.surface == null) return
nativeRenderer?.nativeRender(holder.surface)
handler.postDelayed(this, frameDelayMs)
}
}
private fun loadMediaFiles() {
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallPapers")
val supportedExtensions = videoExtensions + imageExtensions
mediaFiles = if (mediaDir.exists() && mediaDir.isDirectory) {
mediaDir.listFiles()?.filter { supportedExtensions.contains(it.extension.lowercase()) }?.toList() ?: emptyList()
} else {
emptyList()
}
// --- 디버깅 로그 추가 ---
Blog.LOGE("MyWallpaperService", "Found ${mediaFiles.size} media files.")
if (mediaFiles.isNotEmpty()) {
currentMediaIndex = 0
val firstFilePath = mediaFiles[currentMediaIndex].absolutePath
Blog.LOGE("MyWallpaperService", "Attempting to load initial media: $firstFilePath")
getFdFromPath(firstFilePath)?.let { fd ->
Blog.LOGE("MyWallpaperService", "Successfully got fd ($fd) for initial media. Calling nativeSetCurrentMedia.")
nativeRenderer?.nativeSetCurrentMedia(fd)
} ?: run {
// 'let' 블록이 실행되지 않으면 fd가 null이라는 의미
Blog.LOGE("MyWallpaperService", "Failed to get fd for initial media: $firstFilePath")
}
if (mediaFiles.size > 1) {
// Preload 로직도 동일하게 로그 추가 가능
val nextFilePath = mediaFiles[1].absolutePath
Blog.LOGE("MyWallpaperService", "Attempting to preload next media: $nextFilePath")
getFdFromPath(nextFilePath)?.let { fd ->
Blog.LOGE("MyWallpaperService", "Successfully got fd ($fd) for preload. Calling nativeStartNextPreload.")
nativeRenderer?.nativeStartNextPreload(fd)
} ?: run {
Blog.LOGE("MyWallpaperService", "Failed to get fd for preload media: $nextFilePath")
}
}
} else {
Blog.LOGE("MyWallpaperService", "Media files list is empty. No media will be loaded.")
}
}
private val nextMediaCallback = object : NativeRenderer.NextMediaCallback {
override fun onNextMediaRequested() {
if (mediaFiles.isEmpty()) return
currentMediaIndex = (currentMediaIndex + 1) % mediaFiles.size
// 다음 파일 예비 로딩 시에도 파일 디스크립터를 전달
getFdFromPath(mediaFiles[currentMediaIndex].absolutePath)?.let { fd ->
nativeRenderer?.nativeStartNextPreload(fd)
}
}
}
// 파일 경로로 MediaStore를 쿼리하여 File Descriptor를 얻는 함수
private fun getFdFromPath(path: String): Int? {
var fileDescriptor: Int? = null
val uri = MediaStore.Files.getContentUri("external")
val projection = arrayOf(MediaStore.Files.FileColumns._ID)
val selection = "${MediaStore.Files.FileColumns.DATA} = ?"
val selectionArgs = arrayOf(path)
try {
contentResolver.query(uri, projection, selection, selectionArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
val fileUri = ContentUris.withAppendedId(uri, id)
contentResolver.openFileDescriptor(fileUri, "r")?.use { pfd ->
fileDescriptor = pfd.detachFd() // 중요: fd 소유권을 네이티브로 넘기기 위해 detach
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return fileDescriptor
}
override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(surfaceHolder)
holder = surfaceHolder
@@ -41,11 +122,16 @@ class MyWallpaperService : WallpaperService() {
override fun onSurfaceCreated(holder: SurfaceHolder) {
super.onSurfaceCreated(holder)
nativeRenderer = NativeRenderer()
nativeRenderer?.nativeInit(holder.surface)
nativeRenderer?.nativeInit()
handlerThread = HandlerThread("NativeRenderThread").apply { start() }
handler = Handler(handlerThread.looper)
running = true
// 콜백 등록 및 최초 미디어 로딩
nativeRenderer?.nativeSetNextMediaCallback(nextMediaCallback)
loadMediaFiles()
running = true
handler.post(renderRunnable)
}
@@ -57,17 +143,6 @@ class MyWallpaperService : WallpaperService() {
super.onSurfaceDestroyed(holder)
}
private fun loadMediaFiles() {
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallpapers")
val supportedExtensions = videoExtensions + imageExtensions
mediaFiles = if (mediaDir.exists() && mediaDir.isDirectory) {
mediaDir.listFiles()?.filter { supportedExtensions.contains(it.extension.lowercase()) }?.toList() ?: emptyList()
} else {
emptyList()
}
val mediaPaths = mediaFiles.map { it.absolutePath }
nativeRenderer?.nativeSetMediaList(mediaPaths.toTypedArray())
}
}
}
}
@@ -4,15 +4,25 @@ import android.view.Surface
class NativeRenderer {
interface NextMediaCallback {
fun onNextMediaRequested()
}
companion object {
init {
System.loadLibrary("native_renderer")
}
}
external fun nativeInit(surface: Surface?): Boolean
external fun nativeInit(): Boolean
external fun nativeDestroy()
external fun nativeRender()
external fun nativeSetMediaList(paths: Array<String?>?)
external fun nativeRender(surface: Surface)
// ...
// external fun nativeSetCurrentMedia(path: String) // 기존 코드
external fun nativeSetCurrentMedia(fd: Int) // 수정
}
// external fun nativeStartNextPreload(path: String) // 기존 코드
external fun nativeStartNextPreload(fd: Int) // 수정
// ...
external fun nativeSetNextMediaCallback(callback: NextMediaCallback)
}
@@ -65,7 +65,7 @@ class LocationUpdateService : Service(), LocationListener {
try {
//////-1002450229641
val url = PrefString.locationApi.get()
Blog.LOGE("LocationLog ${url}")
// Blog.LOGE("LocationLog ${url}")
if (url.length > 10) {
val client = OkHttpClient.Builder()
.connectionPool(ConnectionPool(5, 60, TimeUnit.SECONDS))
@@ -84,7 +84,7 @@ class LocationUpdateService : Service(), LocationListener {
)
val request: Request = builder.build()
Blog.LOGE("telegram before request ")
// Blog.LOGE("telegram before request ")
// OkHttp 클라이언트로 GET 요청 객체 전송
val response: Response = client.newCall(request).execute()
if (response.isSuccessful()) {