This commit is contained in:
lunaticbum 2025-07-24 18:34:57 +09:00
parent e979b78fb5
commit ee7cee7638

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF
import android.media.MediaCodec import android.media.MediaCodec
import android.media.MediaExtractor import android.media.MediaExtractor
import android.media.MediaFormat import android.media.MediaFormat
@ -13,7 +14,9 @@ import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicYuvToRGB import android.renderscript.ScriptIntrinsicYuvToRGB
import android.renderscript.Type import android.renderscript.Type
import android.service.wallpaper.WallpaperService import android.service.wallpaper.WallpaperService
import android.util.DisplayMetrics
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.WindowManager
import bums.lunatic.launcher.R import bums.lunatic.launcher.R
@ -26,14 +29,33 @@ class MyWallpaperService : WallpaperService() {
lateinit var holder: SurfaceHolder lateinit var holder: SurfaceHolder
private var renderThread: RenderThread? = null private var renderThread: RenderThread? = null
private var screenWidth: Int = 0
private var screenHeight: Int = 0
override fun onCreate(surfaceHolder: SurfaceHolder) { override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(surfaceHolder) super.onCreate(surfaceHolder)
this.holder = surfaceHolder this.holder = surfaceHolder
val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
val metrics = DisplayMetrics()
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
// Android 11 이상 권장 방법
val display = wm.currentWindowMetrics
// Insets(네비게이션바 등 제외 영역 계산 가능), 필요한 경우 처리
val bounds = display.bounds
screenWidth = bounds.width()
screenHeight = bounds.height()
} else {
// 하위 버전 처리 방법
@Suppress("DEPRECATION")
wm.defaultDisplay.getMetrics(metrics)
screenWidth = metrics.widthPixels
screenHeight = metrics.heightPixels
}
} }
override fun onSurfaceCreated(holder: SurfaceHolder) { override fun onSurfaceCreated(holder: SurfaceHolder) {
super.onSurfaceCreated(holder) super.onSurfaceCreated(holder)
renderThread = RenderThread(holder, getApplicationContext()) renderThread = RenderThread(holder, getApplicationContext(), screenWidth, screenHeight)
renderThread?.start() renderThread?.start()
} }
@ -46,7 +68,9 @@ class MyWallpaperService : WallpaperService() {
} }
class RenderThread( class RenderThread(
private val holder: SurfaceHolder, private val holder: SurfaceHolder,
private val context: Context private val context: Context,
private val screenWidth: Int,
private val screenHeight: Int
) : Thread() { ) : Thread() {
private var running = true private var running = true
@ -59,14 +83,28 @@ class RenderThread(
private var rs: RenderScript? = null private var rs: RenderScript? = null
private var yuvToRgb: ScriptIntrinsicYuvToRGB? = null private var yuvToRgb: ScriptIntrinsicYuvToRGB? = null
private var width = 0 private var videoWidth = 0
private var height = 0 private var videoHeight = 0
// 왕복 이동 관련 변수
private var offset = 0f
private var direction = 1
private val speed = 2.5f // 픽셀/프레임, 효과 속도 조절
// 비율 맞춤 렌더 사이즈와 경계
private var renderWidth = 0f
private var renderHeight = 0f
private var renderStartX = 0f
private var renderStartY = 0f
private var maxOffset = 0f
private var moveXAxis = false
override fun run() { override fun run() {
try { try {
// 1. MediaExtractor 초기화 // 1. MediaExtractor 초기화
extractor = MediaExtractor().apply { extractor = MediaExtractor().apply {
val afd = context.resources.openRawResourceFd(R.raw.sample) // 영상 소스 위치에 따라 아래 setDataSource 예시를 변경하십시오.
val afd = context.resources.openRawResourceFd(R.raw.sample) // ex) res/raw/sample.mp4
setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
afd.close() afd.close()
for (i in 0 until trackCount) { for (i in 0 until trackCount) {
@ -75,19 +113,16 @@ class RenderThread(
if (mime.startsWith("video/")) { if (mime.startsWith("video/")) {
videoTrackIndex = i videoTrackIndex = i
selectTrack(i) selectTrack(i)
width = format.getInteger(MediaFormat.KEY_WIDTH) videoWidth = format.getInteger(MediaFormat.KEY_WIDTH)
height = format.getInteger(MediaFormat.KEY_HEIGHT) videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT)
break break
} }
} }
} }
if (videoTrackIndex < 0) { if (videoTrackIndex < 0) return
// 비디오 트랙 없음 종료
return
}
// 2. MediaCodec 초기화 (Surface가 아닌 직접 버퍼 받을 것) // 2. MediaCodec 초기화
extractor?.getTrackFormat(videoTrackIndex)?.let { format -> extractor?.getTrackFormat(videoTrackIndex)?.let { format ->
codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!).apply { codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!).apply {
configure(format, null, null, 0) configure(format, null, null, 0)
@ -99,8 +134,26 @@ class RenderThread(
rs = RenderScript.create(context) rs = RenderScript.create(context)
yuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) yuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
// 4. 비율 맞춤 Scaling 및 이동 관련 변수 설정
val scale = if (videoWidth > videoHeight) {
// 가로가 더 긴 경우: 화면 세로에 맞춤
screenHeight.toFloat() / videoHeight
} else {
// 세로가 더 긴 경우: 화면 가로에 맞춤
screenWidth.toFloat() / videoWidth
}
renderWidth = videoWidth * scale
renderHeight = videoHeight * scale
renderStartX = (screenWidth - renderWidth) / 2f
renderStartY = (screenHeight - renderHeight) / 2f
moveXAxis = renderWidth > screenWidth
maxOffset = if (moveXAxis) renderWidth - screenWidth else if (renderHeight > screenHeight) renderHeight - screenHeight else 0f
var isEOS = false var isEOS = false
while (running && !isInterrupted) { while (running && !isInterrupted) {
// 5. MediaCodec 프레임 디코딩
if (!isEOS) { if (!isEOS) {
val inputBufferIndex = codec?.dequeueInputBuffer(10000) ?: -1 val inputBufferIndex = codec?.dequeueInputBuffer(10000) ?: -1
if (inputBufferIndex >= 0) { if (inputBufferIndex >= 0) {
@ -109,16 +162,11 @@ class RenderThread(
val sampleSize = extractor?.readSampleData(inputBuffer, 0) ?: -1 val sampleSize = extractor?.readSampleData(inputBuffer, 0) ?: -1
if (sampleSize < 0) { if (sampleSize < 0) {
codec?.queueInputBuffer( codec?.queueInputBuffer(
inputBufferIndex, inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
isEOS = true isEOS = true
} else { } else {
val presentationTimeUs = extractor?.sampleTime ?: 0L val pts = extractor?.sampleTime ?: 0L
codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0) codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, pts, 0)
extractor?.advance() extractor?.advance()
} }
} }
@ -132,42 +180,51 @@ class RenderThread(
val yuvData = ByteArray(bufferInfo.size) val yuvData = ByteArray(bufferInfo.size)
outputBuffer.get(yuvData) outputBuffer.get(yuvData)
// 4. RenderScript로 YUV → Bitmap 변환 val bitmap = convertYUVToBitmap(yuvData, videoWidth, videoHeight)
val bitmap = convertYUVToBitmap(yuvData, width, height)
// 5. Canvas에 그리기 // 6. 왕복 이동 애니메이션 offset 계산
if (maxOffset > 0f) {
offset += direction * speed
if (offset < 0f) { offset = 0f; direction = 1 }
if (offset > maxOffset) { offset = maxOffset; direction = -1 }
}
// 7. Canvas에 그리고 unlock
val canvas = holder.lockCanvas() val canvas = holder.lockCanvas()
if (canvas != null) { if (canvas != null) {
canvas.drawBitmap(bitmap, 0f, 0f, null) val drawX = if (moveXAxis) renderStartX - offset else renderStartX
val drawY = if (!moveXAxis) renderStartY - offset else renderStartY
// 오버레이 예: 텍스트 그리기 val dstRect = RectF(
drawX, drawY,
drawX + renderWidth, drawY + renderHeight
)
canvas.drawColor(Color.BLACK)
canvas.drawBitmap(bitmap, null, dstRect, null)
// 오버레이 예시
val paint = Paint().apply { val paint = Paint().apply {
color = Color.WHITE color = Color.WHITE
textSize = 40f textSize = 40f
setShadowLayer(4f, 2f, 2f, Color.DKGRAY)
} }
canvas.drawText("Live Wallpaper Overlay", 50f, 50f, paint) canvas.drawText("Live Wallpaper Overlay", 40f, 80f, paint)
holder.unlockCanvasAndPost(canvas) holder.unlockCanvasAndPost(canvas)
} }
outputBuffer.clear() outputBuffer.clear()
} }
codec?.releaseOutputBuffer(outputBufferIndex, false) codec?.releaseOutputBuffer(outputBufferIndex, false)
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// format 변경 시 처리할 것 (필요시) // 필요하다면 포맷 변경 처리
} }
sleep(1000L/45L) // 약 30fps
// 프레임 타이밍 조절 (예: 30fps 제한)
sleep(33)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} finally { } finally {
// 리소스 해제 try {
codec?.stop() codec?.stop(); codec?.release()
codec?.release() extractor?.release()
extractor?.release() rs?.destroy()
rs?.destroy() } catch (e: Exception) { }
} }
} }