This commit is contained in:
2025-07-24 18:19:30 +09:00
parent 5a6dad2f24
commit e979b78fb5
5 changed files with 212 additions and 0 deletions
@@ -0,0 +1,193 @@
package bums.lunatic.launcher.wall
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Paint
import android.media.MediaCodec
import android.media.MediaExtractor
import android.media.MediaFormat
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicYuvToRGB
import android.renderscript.Type
import android.service.wallpaper.WallpaperService
import android.view.SurfaceHolder
import bums.lunatic.launcher.R
class MyWallpaperService : WallpaperService() {
override fun onCreateEngine(): Engine {
return VideoEngine()
}
inner class VideoEngine : Engine() {
lateinit var holder: SurfaceHolder
private var renderThread: RenderThread? = null
override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(surfaceHolder)
this.holder = surfaceHolder
}
override fun onSurfaceCreated(holder: SurfaceHolder) {
super.onSurfaceCreated(holder)
renderThread = RenderThread(holder, getApplicationContext())
renderThread?.start()
}
override fun onSurfaceDestroyed(holder: SurfaceHolder?) {
super.onSurfaceDestroyed(holder)
renderThread?.interrupt()
renderThread = null
}
}
}
class RenderThread(
private val holder: SurfaceHolder,
private val context: Context
) : Thread() {
private var running = true
private var extractor: MediaExtractor? = null
private var codec: MediaCodec? = null
private var videoTrackIndex: Int = -1
private val bufferInfo = MediaCodec.BufferInfo()
private var rs: RenderScript? = null
private var yuvToRgb: ScriptIntrinsicYuvToRGB? = null
private var width = 0
private var height = 0
override fun run() {
try {
// 1. MediaExtractor 초기화
extractor = MediaExtractor().apply {
val afd = context.resources.openRawResourceFd(R.raw.sample)
setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
afd.close()
for (i in 0 until trackCount) {
val format = getTrackFormat(i)
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
if (mime.startsWith("video/")) {
videoTrackIndex = i
selectTrack(i)
width = format.getInteger(MediaFormat.KEY_WIDTH)
height = format.getInteger(MediaFormat.KEY_HEIGHT)
break
}
}
}
if (videoTrackIndex < 0) {
// 비디오 트랙 없음 종료
return
}
// 2. MediaCodec 초기화 (Surface가 아닌 직접 버퍼 받을 것)
extractor?.getTrackFormat(videoTrackIndex)?.let { format ->
codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!).apply {
configure(format, null, null, 0)
start()
}
}
// 3. RenderScript 초기화
rs = RenderScript.create(context)
yuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
var isEOS = false
while (running && !isInterrupted) {
if (!isEOS) {
val inputBufferIndex = codec?.dequeueInputBuffer(10000) ?: -1
if (inputBufferIndex >= 0) {
val inputBuffer = codec?.getInputBuffer(inputBufferIndex)
if (inputBuffer != null) {
val sampleSize = extractor?.readSampleData(inputBuffer, 0) ?: -1
if (sampleSize < 0) {
codec?.queueInputBuffer(
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)
extractor?.advance()
}
}
}
}
val outputBufferIndex = codec?.dequeueOutputBuffer(bufferInfo, 10000) ?: -1
if (outputBufferIndex >= 0) {
val outputBuffer = codec?.getOutputBuffer(outputBufferIndex)
if (bufferInfo.size > 0 && outputBuffer != null) {
val yuvData = ByteArray(bufferInfo.size)
outputBuffer.get(yuvData)
// 4. RenderScript로 YUV → Bitmap 변환
val bitmap = convertYUVToBitmap(yuvData, width, height)
// 5. Canvas에 그리기
val canvas = holder.lockCanvas()
if (canvas != null) {
canvas.drawBitmap(bitmap, 0f, 0f, null)
// 오버레이 예: 텍스트 그리기
val paint = Paint().apply {
color = Color.WHITE
textSize = 40f
}
canvas.drawText("Live Wallpaper Overlay", 50f, 50f, paint)
holder.unlockCanvasAndPost(canvas)
}
outputBuffer.clear()
}
codec?.releaseOutputBuffer(outputBufferIndex, false)
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// format 변경 시 처리할 것 (필요시)
}
// 프레임 타이밍 조절 (예: 30fps 제한)
sleep(33)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
// 리소스 해제
codec?.stop()
codec?.release()
extractor?.release()
rs?.destroy()
}
}
private fun convertYUVToBitmap(yuvByteArray: ByteArray, width: Int, height: Int): Bitmap {
val yuvType = Type.Builder(rs, Element.U8(rs)).setX(yuvByteArray.size)
val inAllocation = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT)
val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height)
val outAllocation = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT)
inAllocation.copyFrom(yuvByteArray)
yuvToRgb?.setInput(inAllocation)
yuvToRgb?.forEach(outAllocation)
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
outAllocation.copyTo(bitmap)
inAllocation.destroy()
outAllocation.destroy()
return bitmap
}
}