...
This commit is contained in:
@@ -49,6 +49,7 @@ android {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user