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.Color
import android.graphics.Paint
import android.graphics.RectF
import android.media.MediaCodec
import android.media.MediaExtractor
import android.media.MediaFormat
@ -13,7 +14,9 @@ import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicYuvToRGB
import android.renderscript.Type
import android.service.wallpaper.WallpaperService
import android.util.DisplayMetrics
import android.view.SurfaceHolder
import android.view.WindowManager
import bums.lunatic.launcher.R
@ -26,14 +29,33 @@ class MyWallpaperService : WallpaperService() {
lateinit var holder: SurfaceHolder
private var renderThread: RenderThread? = null
private var screenWidth: Int = 0
private var screenHeight: Int = 0
override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(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) {
super.onSurfaceCreated(holder)
renderThread = RenderThread(holder, getApplicationContext())
renderThread = RenderThread(holder, getApplicationContext(), screenWidth, screenHeight)
renderThread?.start()
}
@ -46,7 +68,9 @@ class MyWallpaperService : WallpaperService() {
}
class RenderThread(
private val holder: SurfaceHolder,
private val context: Context
private val context: Context,
private val screenWidth: Int,
private val screenHeight: Int
) : Thread() {
private var running = true
@ -59,14 +83,28 @@ class RenderThread(
private var rs: RenderScript? = null
private var yuvToRgb: ScriptIntrinsicYuvToRGB? = null
private var width = 0
private var height = 0
private var videoWidth = 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() {
try {
// 1. MediaExtractor 초기화
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)
afd.close()
for (i in 0 until trackCount) {
@ -75,19 +113,16 @@ class RenderThread(
if (mime.startsWith("video/")) {
videoTrackIndex = i
selectTrack(i)
width = format.getInteger(MediaFormat.KEY_WIDTH)
height = format.getInteger(MediaFormat.KEY_HEIGHT)
videoWidth = format.getInteger(MediaFormat.KEY_WIDTH)
videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT)
break
}
}
}
if (videoTrackIndex < 0) {
// 비디오 트랙 없음 종료
return
}
if (videoTrackIndex < 0) return
// 2. MediaCodec 초기화 (Surface가 아닌 직접 버퍼 받을 것)
// 2. MediaCodec 초기화
extractor?.getTrackFormat(videoTrackIndex)?.let { format ->
codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!).apply {
configure(format, null, null, 0)
@ -99,8 +134,26 @@ class RenderThread(
rs = RenderScript.create(context)
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
while (running && !isInterrupted) {
// 5. MediaCodec 프레임 디코딩
if (!isEOS) {
val inputBufferIndex = codec?.dequeueInputBuffer(10000) ?: -1
if (inputBufferIndex >= 0) {
@ -109,16 +162,11 @@ class RenderThread(
val sampleSize = extractor?.readSampleData(inputBuffer, 0) ?: -1
if (sampleSize < 0) {
codec?.queueInputBuffer(
inputBufferIndex,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
isEOS = true
} else {
val presentationTimeUs = extractor?.sampleTime ?: 0L
codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0)
val pts = extractor?.sampleTime ?: 0L
codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, pts, 0)
extractor?.advance()
}
}
@ -132,42 +180,51 @@ class RenderThread(
val yuvData = ByteArray(bufferInfo.size)
outputBuffer.get(yuvData)
// 4. RenderScript로 YUV → Bitmap 변환
val bitmap = convertYUVToBitmap(yuvData, width, height)
val bitmap = convertYUVToBitmap(yuvData, videoWidth, videoHeight)
// 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()
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 {
color = Color.WHITE
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)
}
outputBuffer.clear()
}
codec?.releaseOutputBuffer(outputBufferIndex, false)
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// format 변경 시 처리할 것 (필요시)
// 필요하다면 포맷 변경 처리
}
// 프레임 타이밍 조절 (예: 30fps 제한)
sleep(33)
sleep(1000L/45L) // 약 30fps
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
// 리소스 해제
codec?.stop()
codec?.release()
extractor?.release()
rs?.destroy()
try {
codec?.stop(); codec?.release()
extractor?.release()
rs?.destroy()
} catch (e: Exception) { }
}
}