|
|
|
@@ -1,686 +1,241 @@
|
|
|
|
|
package bums.lunatic.launcher.wall
|
|
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.graphics.Bitmap
|
|
|
|
|
import android.graphics.BitmapFactory
|
|
|
|
|
import android.graphics.Canvas
|
|
|
|
|
import android.graphics.Color
|
|
|
|
|
import android.graphics.Paint
|
|
|
|
|
import android.graphics.RectF
|
|
|
|
|
import android.media.MediaCodec
|
|
|
|
|
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
|
|
|
|
|
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|
|
|
|
|
import android.media.MediaExtractor
|
|
|
|
|
import android.media.MediaFormat
|
|
|
|
|
import android.os.Environment
|
|
|
|
|
import android.os.Handler
|
|
|
|
|
import android.os.HandlerThread
|
|
|
|
|
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.util.DisplayMetrics
|
|
|
|
|
import android.view.SurfaceHolder
|
|
|
|
|
import android.view.WindowManager
|
|
|
|
|
import bums.lunatic.launcher.R
|
|
|
|
|
import bums.lunatic.launcher.utils.Blog
|
|
|
|
|
import java.io.File
|
|
|
|
|
import kotlin.random.Random
|
|
|
|
|
|
|
|
|
|
class MyWallpaperService : WallpaperService() {
|
|
|
|
|
override fun onCreateEngine(): Engine {
|
|
|
|
|
return VideoEngine()
|
|
|
|
|
}
|
|
|
|
|
override fun onCreateEngine(): Engine = NativeRenderEngine()
|
|
|
|
|
|
|
|
|
|
inner class VideoEngine : WallpaperService.Engine() {
|
|
|
|
|
inner class NativeRenderEngine : Engine() {
|
|
|
|
|
private lateinit var holder: SurfaceHolder
|
|
|
|
|
private lateinit var renderLooper: RenderLooper
|
|
|
|
|
private lateinit var handlerThread: HandlerThread
|
|
|
|
|
private lateinit var handler: Handler
|
|
|
|
|
private var running = false
|
|
|
|
|
|
|
|
|
|
private var screenWidth: Int = 0
|
|
|
|
|
private var screenHeight: Int = 0
|
|
|
|
|
private var nativeRenderer: NativeRenderer? = null
|
|
|
|
|
|
|
|
|
|
private var currentBitmap: Bitmap? = null
|
|
|
|
|
private var currentBitmapPixels: IntArray? = null
|
|
|
|
|
private var currentBitmapWidth = 0
|
|
|
|
|
private var currentBitmapHeight = 0
|
|
|
|
|
|
|
|
|
|
private var screenWidth = 0
|
|
|
|
|
private var screenHeight = 0
|
|
|
|
|
|
|
|
|
|
private var renderWidth = 0f
|
|
|
|
|
private var renderHeight = 0f
|
|
|
|
|
|
|
|
|
|
private var maxOffset = 0f
|
|
|
|
|
private var moveXAxis = false
|
|
|
|
|
private var moveYAxis = false
|
|
|
|
|
|
|
|
|
|
private var offsetX = 0f
|
|
|
|
|
private var offsetY = 0f
|
|
|
|
|
private var movingForward = true
|
|
|
|
|
|
|
|
|
|
private var fadeAlpha = 1f
|
|
|
|
|
private val fadeDuration = 3000L // 3초
|
|
|
|
|
private val imageDisplayDuration = 20_000L // 20초
|
|
|
|
|
|
|
|
|
|
private var currentMediaStartTime = 0L
|
|
|
|
|
private val frameDelayMs = 16L
|
|
|
|
|
|
|
|
|
|
private val renderRunnable = object : Runnable {
|
|
|
|
|
override fun run() {
|
|
|
|
|
if (!running) return
|
|
|
|
|
|
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
val mediaElapsedMs = now - currentMediaStartTime
|
|
|
|
|
|
|
|
|
|
// 이동 애니메이션 계산
|
|
|
|
|
val travelDistance = maxOffset
|
|
|
|
|
val speed = if (travelDistance > 0f) (travelDistance * 2) / imageDisplayDuration.toFloat() else 0f
|
|
|
|
|
val deltaOffset = speed * frameDelayMs
|
|
|
|
|
|
|
|
|
|
if (moveXAxis) {
|
|
|
|
|
if (movingForward) {
|
|
|
|
|
offsetX += deltaOffset
|
|
|
|
|
if (offsetX >= maxOffset) offsetX = maxOffset.also { movingForward = false }
|
|
|
|
|
} else {
|
|
|
|
|
offsetX -= deltaOffset
|
|
|
|
|
if (offsetX <= 0f) offsetX = 0f.also { movingForward = true }
|
|
|
|
|
}
|
|
|
|
|
offsetY = 0f
|
|
|
|
|
} else if (moveYAxis) {
|
|
|
|
|
if (movingForward) {
|
|
|
|
|
offsetY += deltaOffset
|
|
|
|
|
if (offsetY >= maxOffset) offsetY = maxOffset.also { movingForward = false }
|
|
|
|
|
} else {
|
|
|
|
|
offsetY -= deltaOffset
|
|
|
|
|
if (offsetY <= 0f) offsetY = 0f.also { movingForward = true }
|
|
|
|
|
}
|
|
|
|
|
offsetX = 0f
|
|
|
|
|
} else {
|
|
|
|
|
offsetX = 0f
|
|
|
|
|
offsetY = 0f
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 페이드 인/아웃 계산
|
|
|
|
|
fadeAlpha = if (mediaElapsedMs >= imageDisplayDuration - fadeDuration) {
|
|
|
|
|
val elapsed = mediaElapsedMs - (imageDisplayDuration - fadeDuration)
|
|
|
|
|
1f - (elapsed.toFloat() / fadeDuration)
|
|
|
|
|
} else 1f
|
|
|
|
|
if (fadeAlpha < 0f) fadeAlpha = 0f
|
|
|
|
|
|
|
|
|
|
// 비트맵에서 픽셀 추출
|
|
|
|
|
currentBitmap?.let { bmp ->
|
|
|
|
|
if (currentBitmapPixels == null || currentBitmapWidth != bmp.width || currentBitmapHeight != bmp.height) {
|
|
|
|
|
currentBitmapWidth = bmp.width
|
|
|
|
|
currentBitmapHeight = bmp.height
|
|
|
|
|
currentBitmapPixels = IntArray(currentBitmapWidth * currentBitmapHeight)
|
|
|
|
|
}
|
|
|
|
|
bmp.getPixels(currentBitmapPixels!!, 0, currentBitmapWidth, 0, 0, currentBitmapWidth, currentBitmapHeight)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 네이티브 렌더 호출
|
|
|
|
|
currentBitmapPixels?.let { pixels ->
|
|
|
|
|
nativeRenderer?.nativeRender(pixels, currentBitmapWidth, currentBitmapHeight, offsetX, offsetY, fadeAlpha)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 20초 후 미디어 교체
|
|
|
|
|
if (mediaElapsedMs >= imageDisplayDuration) {
|
|
|
|
|
loadNextMedia()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler.postDelayed(this, frameDelayMs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onCreate(surfaceHolder: SurfaceHolder) {
|
|
|
|
|
super.onCreate(surfaceHolder)
|
|
|
|
|
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) {
|
|
|
|
|
val display = wm.currentWindowMetrics
|
|
|
|
|
screenWidth = display.bounds.width()
|
|
|
|
|
screenHeight = display.bounds.height()
|
|
|
|
|
} else {
|
|
|
|
|
@Suppress("DEPRECATION")
|
|
|
|
|
wm.defaultDisplay.getMetrics(metrics)
|
|
|
|
|
screenWidth = metrics.widthPixels
|
|
|
|
|
screenHeight = metrics.heightPixels
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onSurfaceCreated(holder: SurfaceHolder) {
|
|
|
|
|
super.onSurfaceCreated(holder)
|
|
|
|
|
handlerThread = HandlerThread("RenderHandlerThread")
|
|
|
|
|
handlerThread.start()
|
|
|
|
|
renderLooper = RenderLooper(this@MyWallpaperService, holder, screenWidth, screenHeight, handlerThread)
|
|
|
|
|
renderLooper.start()
|
|
|
|
|
nativeRenderer = NativeRenderer()
|
|
|
|
|
nativeRenderer?.nativeInit(holder.surface)
|
|
|
|
|
|
|
|
|
|
handlerThread = HandlerThread("NativeRenderThread").apply { start() }
|
|
|
|
|
handler = Handler(handlerThread.looper)
|
|
|
|
|
running = true
|
|
|
|
|
|
|
|
|
|
val wm = getSystemService(WINDOW_SERVICE) as android.view.WindowManager
|
|
|
|
|
val metrics = android.util.DisplayMetrics()
|
|
|
|
|
wm.defaultDisplay.getMetrics(metrics)
|
|
|
|
|
screenWidth = metrics.widthPixels
|
|
|
|
|
screenHeight = metrics.heightPixels
|
|
|
|
|
|
|
|
|
|
loadMediaFiles()
|
|
|
|
|
|
|
|
|
|
handler.post(renderRunnable)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onSurfaceDestroyed(holder: SurfaceHolder) {
|
|
|
|
|
super.onSurfaceDestroyed(holder)
|
|
|
|
|
renderLooper.stop()
|
|
|
|
|
running = false
|
|
|
|
|
handler.removeCallbacks(renderRunnable)
|
|
|
|
|
nativeRenderer?.nativeDestroy()
|
|
|
|
|
handlerThread.quitSafely()
|
|
|
|
|
super.onSurfaceDestroyed(holder)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onSurfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
|
|
|
|
super.onSurfaceChanged(holder, format, width, height)
|
|
|
|
|
screenWidth = width
|
|
|
|
|
screenHeight = height
|
|
|
|
|
renderLooper.updateScreenSize(width, height)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private var mediaFiles: List<File> = emptyList()
|
|
|
|
|
private var currentIndex = 0
|
|
|
|
|
|
|
|
|
|
interface Renderer {
|
|
|
|
|
fun render(canvas: Canvas)
|
|
|
|
|
fun release()
|
|
|
|
|
fun updateScreenSize(width: Int, height: Int)
|
|
|
|
|
fun resetOffset()
|
|
|
|
|
fun update()
|
|
|
|
|
fun isCycleCompleted(): Boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ImageRenderer(
|
|
|
|
|
private val context: Context,
|
|
|
|
|
private var bitmap: Bitmap,
|
|
|
|
|
private var screenWidth: Int,
|
|
|
|
|
private var screenHeight: Int
|
|
|
|
|
) : Renderer {
|
|
|
|
|
|
|
|
|
|
private var renderWidth = 0f
|
|
|
|
|
private var renderHeight = 0f
|
|
|
|
|
private var renderStartX = 0f
|
|
|
|
|
private var renderStartY = 0f
|
|
|
|
|
|
|
|
|
|
private var moveXAxis = false
|
|
|
|
|
private var moveYAxis = false
|
|
|
|
|
private var maxOffset = 0f
|
|
|
|
|
|
|
|
|
|
private var offset = 0f
|
|
|
|
|
private var direction = 1
|
|
|
|
|
private val speed = 2.5f
|
|
|
|
|
|
|
|
|
|
private val holdDurationMs = 10_000L
|
|
|
|
|
private var noMoveStartTime: Long = 0L
|
|
|
|
|
private var moveCycleCompleted = false
|
|
|
|
|
|
|
|
|
|
private val paintOverlay = Paint().apply {
|
|
|
|
|
color = Color.WHITE
|
|
|
|
|
textSize = 40f
|
|
|
|
|
setShadowLayer(4f, 2f, 2f, Color.DKGRAY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
setupScaling(bitmap.width, bitmap.height)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun render(canvas: Canvas) {
|
|
|
|
|
updateOffset()
|
|
|
|
|
val drawX = if (moveXAxis) renderStartX - offset else renderStartX
|
|
|
|
|
val drawY = if (moveYAxis) renderStartY - offset else renderStartY
|
|
|
|
|
val dstRect = RectF(drawX, drawY, drawX + renderWidth, drawY + renderHeight)
|
|
|
|
|
|
|
|
|
|
canvas.drawColor(Color.BLACK)
|
|
|
|
|
canvas.drawBitmap(bitmap, null, dstRect, null)
|
|
|
|
|
// canvas.drawText("Live Wallpaper Overlay", 40f, 80f, paintOverlay)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun release() {
|
|
|
|
|
bitmap.recycle()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun updateScreenSize(width: Int, height: Int) {
|
|
|
|
|
screenWidth = width
|
|
|
|
|
screenHeight = height
|
|
|
|
|
setupScaling(bitmap.width, bitmap.height)
|
|
|
|
|
resetOffset()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun resetOffset() {
|
|
|
|
|
offset = 0f
|
|
|
|
|
direction = 1
|
|
|
|
|
noMoveStartTime = 0L
|
|
|
|
|
moveCycleCompleted = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun update() {
|
|
|
|
|
if (!moveXAxis && !moveYAxis) {
|
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
if (noMoveStartTime == 0L) noMoveStartTime = now
|
|
|
|
|
else if (now - noMoveStartTime >= holdDurationMs) {
|
|
|
|
|
noMoveStartTime = 0L
|
|
|
|
|
moveCycleCompleted = true
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (moveCycleCompleted) {
|
|
|
|
|
moveCycleCompleted = false
|
|
|
|
|
}
|
|
|
|
|
noMoveStartTime = 0L
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun isCycleCompleted(): Boolean = moveCycleCompleted
|
|
|
|
|
|
|
|
|
|
private fun setupScaling(mediaWidth: Int, mediaHeight: Int) {
|
|
|
|
|
val isLandscape = mediaWidth > mediaHeight
|
|
|
|
|
|
|
|
|
|
var scale = if (isLandscape) {
|
|
|
|
|
// 가로가 긴 경우 => 세로 기준 스케일링
|
|
|
|
|
screenHeight.toFloat() / mediaHeight
|
|
|
|
|
} else {
|
|
|
|
|
// 세로가 긴 경우 => 가로 기준 스케일링
|
|
|
|
|
screenWidth.toFloat() / mediaWidth
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var targetWidth = mediaWidth * scale
|
|
|
|
|
var targetHeight = mediaHeight * scale
|
|
|
|
|
|
|
|
|
|
// 2차 보정: 스케일 후 크기가 기기 해상도보다 작으면 크기 맞춤
|
|
|
|
|
if (targetWidth < screenWidth) {
|
|
|
|
|
val scaleX = screenWidth.toFloat() / mediaWidth
|
|
|
|
|
if (scaleX > scale) {
|
|
|
|
|
scale = scaleX
|
|
|
|
|
targetWidth = mediaWidth * scale
|
|
|
|
|
targetHeight = mediaHeight * scale
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (targetHeight < screenHeight) {
|
|
|
|
|
val scaleY = screenHeight.toFloat() / mediaHeight
|
|
|
|
|
if (scaleY > scale) {
|
|
|
|
|
scale = scaleY
|
|
|
|
|
targetWidth = mediaWidth * scale
|
|
|
|
|
targetHeight = mediaHeight * scale
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderWidth = targetWidth
|
|
|
|
|
renderHeight = targetHeight
|
|
|
|
|
|
|
|
|
|
renderStartX = 0f
|
|
|
|
|
renderStartY = 0f
|
|
|
|
|
|
|
|
|
|
val diffWidth = renderWidth - screenWidth
|
|
|
|
|
val diffHeight = renderHeight - screenHeight
|
|
|
|
|
|
|
|
|
|
moveXAxis = diffWidth > 1f
|
|
|
|
|
moveYAxis = diffHeight > 1f
|
|
|
|
|
|
|
|
|
|
maxOffset = if (moveXAxis) diffWidth else if (moveYAxis) diffHeight else 0f
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun updateOffset() {
|
|
|
|
|
if (moveXAxis || moveYAxis) {
|
|
|
|
|
offset += direction * speed
|
|
|
|
|
if (offset < 0f) {
|
|
|
|
|
offset = 0f
|
|
|
|
|
direction = 1
|
|
|
|
|
}
|
|
|
|
|
if (offset > maxOffset) {
|
|
|
|
|
offset = maxOffset
|
|
|
|
|
direction = -1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class VideoRenderer(
|
|
|
|
|
private val context: Context,
|
|
|
|
|
private val extractor: MediaExtractor,
|
|
|
|
|
private var screenWidth: Int,
|
|
|
|
|
private var screenHeight: Int
|
|
|
|
|
) : Renderer, Runnable {
|
|
|
|
|
private var codec: MediaCodec? = null
|
|
|
|
|
private var videoTrackIndex: Int = -1
|
|
|
|
|
private val bufferInfo = MediaCodec.BufferInfo()
|
|
|
|
|
|
|
|
|
|
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 moveXAxis = false
|
|
|
|
|
private var moveYAxis = false
|
|
|
|
|
private var maxOffset = 0f
|
|
|
|
|
|
|
|
|
|
private var running = true
|
|
|
|
|
private var isEOS = false
|
|
|
|
|
|
|
|
|
|
private var keyFrameRate = 60
|
|
|
|
|
|
|
|
|
|
private var rs: RenderScript? = null
|
|
|
|
|
private var yuvToRgb: ScriptIntrinsicYuvToRGB? = null
|
|
|
|
|
|
|
|
|
|
private var bitmap: Bitmap? = null
|
|
|
|
|
|
|
|
|
|
private val paintOverlay = Paint().apply {
|
|
|
|
|
color = Color.WHITE
|
|
|
|
|
textSize = 40f
|
|
|
|
|
setShadowLayer(4f, 2f, 2f, Color.DKGRAY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var noMoveStartTime: Long = 0L
|
|
|
|
|
private val holdDurationMs = 10_000L
|
|
|
|
|
private var moveCycleCompleted = false
|
|
|
|
|
|
|
|
|
|
private val lock = Object()
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
setupDecoder()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun render(canvas: Canvas) {
|
|
|
|
|
synchronized(lock) {
|
|
|
|
|
if (bitmap != null) {
|
|
|
|
|
val drawX = if (moveXAxis) renderStartX - offset else renderStartX
|
|
|
|
|
val drawY = if (moveYAxis) renderStartY - offset else renderStartY
|
|
|
|
|
val dstRect = RectF(drawX, drawY, drawX + renderWidth, drawY + renderHeight)
|
|
|
|
|
|
|
|
|
|
canvas.drawColor(Color.BLACK)
|
|
|
|
|
canvas.drawBitmap(bitmap!!, null, dstRect, null)
|
|
|
|
|
// canvas.drawText("Live Wallpaper Overlay", 40f, 80f, paintOverlay)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun release() {
|
|
|
|
|
codec?.stop()
|
|
|
|
|
codec?.release()
|
|
|
|
|
extractor.release()
|
|
|
|
|
rs?.destroy()
|
|
|
|
|
bitmap?.recycle()
|
|
|
|
|
running = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun updateScreenSize(width: Int, height: Int) {
|
|
|
|
|
synchronized(lock) {
|
|
|
|
|
screenWidth = width
|
|
|
|
|
screenHeight = height
|
|
|
|
|
setupScaling(videoWidth, videoHeight)
|
|
|
|
|
resetOffset()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun resetOffset() {
|
|
|
|
|
synchronized(lock) {
|
|
|
|
|
offset = 0f
|
|
|
|
|
direction = 1
|
|
|
|
|
noMoveStartTime = 0L
|
|
|
|
|
moveCycleCompleted = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun update() {
|
|
|
|
|
synchronized(lock) {
|
|
|
|
|
if (!moveXAxis && !moveYAxis) {
|
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
if (noMoveStartTime == 0L) noMoveStartTime = now
|
|
|
|
|
else if (now - noMoveStartTime >= holdDurationMs) {
|
|
|
|
|
noMoveStartTime = 0L
|
|
|
|
|
moveCycleCompleted = true
|
|
|
|
|
}
|
|
|
|
|
private fun loadMediaFiles() {
|
|
|
|
|
val mediaDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "wallpapers")
|
|
|
|
|
val extensions = listOf("jpg", "jpeg", "png", "bmp", "webp")
|
|
|
|
|
mediaFiles = if (mediaDir.exists() && mediaDir.isDirectory) {
|
|
|
|
|
mediaDir.listFiles()?.filter { extensions.contains(it.extension.lowercase()) }?.toList() ?: emptyList()
|
|
|
|
|
} else {
|
|
|
|
|
if (moveCycleCompleted) {
|
|
|
|
|
moveCycleCompleted = false
|
|
|
|
|
emptyList()
|
|
|
|
|
}
|
|
|
|
|
loadNextMedia()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun loadNextMedia() {
|
|
|
|
|
if (mediaFiles.isEmpty()) return
|
|
|
|
|
|
|
|
|
|
val file = mediaFiles[currentIndex]
|
|
|
|
|
currentIndex = (currentIndex + 1) % mediaFiles.size
|
|
|
|
|
loadBitmapFromFile(file)
|
|
|
|
|
currentMediaStartTime = System.currentTimeMillis()
|
|
|
|
|
offsetX = 0f
|
|
|
|
|
offsetY = 0f
|
|
|
|
|
movingForward = true
|
|
|
|
|
fadeAlpha = 1f
|
|
|
|
|
|
|
|
|
|
setupScaling(currentBitmapWidth, currentBitmapHeight)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun loadBitmapFromFile(file: File) {
|
|
|
|
|
val originalBitmap = BitmapFactory.decodeFile(file.absolutePath)
|
|
|
|
|
originalBitmap?.let {
|
|
|
|
|
currentBitmap?.recycle()
|
|
|
|
|
|
|
|
|
|
setupScaling(it.width, it.height)
|
|
|
|
|
|
|
|
|
|
val scaledWidth = renderWidth.toInt().coerceAtLeast(1)
|
|
|
|
|
val scaledHeight = renderHeight.toInt().coerceAtLeast(1)
|
|
|
|
|
|
|
|
|
|
val scaledBitmap = Bitmap.createScaledBitmap(it, scaledWidth, scaledHeight, true)
|
|
|
|
|
|
|
|
|
|
currentBitmap = scaledBitmap
|
|
|
|
|
currentBitmapWidth = scaledWidth
|
|
|
|
|
currentBitmapHeight = scaledHeight
|
|
|
|
|
|
|
|
|
|
currentBitmapPixels = IntArray(currentBitmapWidth * currentBitmapHeight)
|
|
|
|
|
scaledBitmap.getPixels(currentBitmapPixels!!, 0, currentBitmapWidth, 0, 0, currentBitmapWidth, currentBitmapHeight)
|
|
|
|
|
|
|
|
|
|
if (it != scaledBitmap) {
|
|
|
|
|
it.recycle()
|
|
|
|
|
}
|
|
|
|
|
noMoveStartTime = 0L
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun isCycleCompleted(): Boolean = moveCycleCompleted
|
|
|
|
|
private fun setupScaling(mediaWidth: Int, mediaHeight: Int) {
|
|
|
|
|
if (screenWidth == 0 || screenHeight == 0) return
|
|
|
|
|
|
|
|
|
|
operator fun invoke() = run()
|
|
|
|
|
val isLandscape = mediaWidth > mediaHeight
|
|
|
|
|
var scale = if (isLandscape) screenHeight.toFloat() / mediaHeight else screenWidth.toFloat() / mediaWidth
|
|
|
|
|
|
|
|
|
|
override fun run() {
|
|
|
|
|
try {
|
|
|
|
|
val inputBuffersTimeoutUs = 10000L
|
|
|
|
|
var targetWidth = mediaWidth * scale
|
|
|
|
|
var targetHeight = mediaHeight * scale
|
|
|
|
|
|
|
|
|
|
while (running && !Thread.currentThread().isInterrupted) {
|
|
|
|
|
if (isEOS) {
|
|
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
if (targetWidth < screenWidth) {
|
|
|
|
|
val scaleX = screenWidth.toFloat() / mediaWidth
|
|
|
|
|
if (scaleX > scale) {
|
|
|
|
|
scale = scaleX
|
|
|
|
|
targetWidth = mediaWidth * scale
|
|
|
|
|
targetHeight = mediaHeight * scale
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val inputBufferIndex = codec?.dequeueInputBuffer(inputBuffersTimeoutUs) ?: -1
|
|
|
|
|
if (inputBufferIndex >= 0) {
|
|
|
|
|
val inputBuffer = codec?.getInputBuffer(inputBufferIndex)
|
|
|
|
|
if (inputBuffer != null) {
|
|
|
|
|
val sampleSize = extractor.readSampleData(inputBuffer, 0)
|
|
|
|
|
if (sampleSize < 0) {
|
|
|
|
|
codec?.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
|
|
|
|
|
isEOS = true
|
|
|
|
|
} else {
|
|
|
|
|
val pts = extractor.sampleTime
|
|
|
|
|
codec?.queueInputBuffer(inputBufferIndex, 0, sampleSize, pts, 0)
|
|
|
|
|
extractor.advance()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (targetHeight < screenHeight) {
|
|
|
|
|
val scaleY = screenHeight.toFloat() / mediaHeight
|
|
|
|
|
if (scaleY > scale) {
|
|
|
|
|
scale = scaleY
|
|
|
|
|
targetWidth = mediaWidth * scale
|
|
|
|
|
targetHeight = mediaHeight * scale
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val outputBufferIndex = codec?.dequeueOutputBuffer(bufferInfo, inputBuffersTimeoutUs) ?: -1
|
|
|
|
|
if (outputBufferIndex >= 0) {
|
|
|
|
|
val outputBuffer = codec?.getOutputBuffer(outputBufferIndex)
|
|
|
|
|
if (bufferInfo.size > 0 && outputBuffer != null) {
|
|
|
|
|
val format = codec?.outputFormat
|
|
|
|
|
|
|
|
|
|
val colorFormat = format?.getInteger(MediaFormat.KEY_COLOR_FORMAT) ?: 0
|
|
|
|
|
|
|
|
|
|
val rawYUV = ByteArray(bufferInfo.size)
|
|
|
|
|
outputBuffer.get(rawYUV)
|
|
|
|
|
outputBuffer.position(0)
|
|
|
|
|
|
|
|
|
|
val nv21Data = when (colorFormat) {
|
|
|
|
|
COLOR_FormatYUV420SemiPlanar -> nv12ToNv21(rawYUV, videoWidth, videoHeight)
|
|
|
|
|
COLOR_FormatYUV420Planar -> i420ToNv21(rawYUV, videoWidth, videoHeight)
|
|
|
|
|
else -> rawYUV
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val bmp = convertYUVToBitmap(nv21Data, videoWidth, videoHeight)
|
|
|
|
|
synchronized(lock) {
|
|
|
|
|
bitmap?.recycle()
|
|
|
|
|
bitmap = bmp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (moveXAxis || moveYAxis) {
|
|
|
|
|
offset += direction * speed
|
|
|
|
|
if (offset < 0f) {
|
|
|
|
|
offset = 0f
|
|
|
|
|
direction = 1
|
|
|
|
|
}
|
|
|
|
|
if (moveXAxis && offset > maxOffset) {
|
|
|
|
|
offset = maxOffset
|
|
|
|
|
direction = -1
|
|
|
|
|
} else if (moveYAxis && offset > maxOffset) {
|
|
|
|
|
offset = maxOffset
|
|
|
|
|
direction = -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
noMoveStartTime = 0L
|
|
|
|
|
} else {
|
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
if (noMoveStartTime == 0L) noMoveStartTime = now
|
|
|
|
|
else if (now - noMoveStartTime >= holdDurationMs) {
|
|
|
|
|
noMoveStartTime = 0L
|
|
|
|
|
moveCycleCompleted = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
codec?.releaseOutputBuffer(outputBufferIndex, false)
|
|
|
|
|
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
|
|
|
// 필요시 처리
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
e.printStackTrace()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun setupDecoder() {
|
|
|
|
|
for (i in 0 until extractor.trackCount) {
|
|
|
|
|
val format = extractor.getTrackFormat(i)
|
|
|
|
|
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
|
|
|
|
|
if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) {
|
|
|
|
|
keyFrameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE)
|
|
|
|
|
} else {
|
|
|
|
|
keyFrameRate = 60
|
|
|
|
|
}
|
|
|
|
|
if (mime.startsWith("video/")) {
|
|
|
|
|
videoTrackIndex = i
|
|
|
|
|
extractor.selectTrack(i)
|
|
|
|
|
videoWidth = format.getInteger(MediaFormat.KEY_WIDTH)
|
|
|
|
|
videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (videoTrackIndex < 0) return
|
|
|
|
|
renderWidth = targetWidth
|
|
|
|
|
renderHeight = targetHeight
|
|
|
|
|
|
|
|
|
|
codec?.stop()
|
|
|
|
|
codec?.release()
|
|
|
|
|
codec = MediaCodec.createDecoderByType(extractor.getTrackFormat(videoTrackIndex).getString(MediaFormat.KEY_MIME)!!).apply {
|
|
|
|
|
configure(extractor.getTrackFormat(videoTrackIndex), null, null, 0)
|
|
|
|
|
start()
|
|
|
|
|
}
|
|
|
|
|
val diffWidth = renderWidth - screenWidth
|
|
|
|
|
val diffHeight = renderHeight - screenHeight
|
|
|
|
|
|
|
|
|
|
rs?.destroy()
|
|
|
|
|
rs = RenderScript.create(context)
|
|
|
|
|
yuvToRgb?.destroy()
|
|
|
|
|
yuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
|
|
|
|
|
moveXAxis = diffWidth > 1f
|
|
|
|
|
moveYAxis = diffHeight > 1f
|
|
|
|
|
|
|
|
|
|
setupScaling(videoWidth, videoHeight)
|
|
|
|
|
resetOffset()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun setupScaling(mediaWidth: Int, mediaHeight: Int) {
|
|
|
|
|
val isLandscape = mediaWidth > mediaHeight
|
|
|
|
|
|
|
|
|
|
var scale = if (isLandscape) {
|
|
|
|
|
screenHeight.toFloat() / mediaHeight
|
|
|
|
|
} else {
|
|
|
|
|
screenWidth.toFloat() / mediaWidth
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var targetWidth = mediaWidth * scale
|
|
|
|
|
var targetHeight = mediaHeight * scale
|
|
|
|
|
|
|
|
|
|
if (targetWidth < screenWidth) {
|
|
|
|
|
val scaleX = screenWidth.toFloat() / mediaWidth
|
|
|
|
|
if (scaleX > scale) {
|
|
|
|
|
scale = scaleX
|
|
|
|
|
targetWidth = mediaWidth * scale
|
|
|
|
|
targetHeight = mediaHeight * scale
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (targetHeight < screenHeight) {
|
|
|
|
|
val scaleY = screenHeight.toFloat() / mediaHeight
|
|
|
|
|
if (scaleY > scale) {
|
|
|
|
|
scale = scaleY
|
|
|
|
|
targetWidth = mediaWidth * scale
|
|
|
|
|
targetHeight = mediaHeight * scale
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderWidth = targetWidth
|
|
|
|
|
renderHeight = targetHeight
|
|
|
|
|
|
|
|
|
|
renderStartX = 0f
|
|
|
|
|
renderStartY = 0f
|
|
|
|
|
|
|
|
|
|
val diffWidth = renderWidth - screenWidth
|
|
|
|
|
val diffHeight = renderHeight - screenHeight
|
|
|
|
|
|
|
|
|
|
moveXAxis = diffWidth > 1f
|
|
|
|
|
moveYAxis = diffHeight > 1f
|
|
|
|
|
|
|
|
|
|
maxOffset = if (moveXAxis) diffWidth else if (moveYAxis) diffHeight else 0f
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun convertYUVToBitmap(yuvByteArray: ByteArray, width: Int, height: Int): Bitmap {
|
|
|
|
|
val minSize = width * height * 3 / 2
|
|
|
|
|
val realData = if (yuvByteArray.size >= minSize) yuvByteArray.copyOf(minSize) else yuvByteArray
|
|
|
|
|
|
|
|
|
|
val yuvType = Type.Builder(rs, Element.U8(rs)).setX(minSize)
|
|
|
|
|
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(realData)
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun nv12ToNv21(nv12: ByteArray, width: Int, height: Int): ByteArray {
|
|
|
|
|
val frameSize = width * height
|
|
|
|
|
val nv21 = ByteArray(frameSize * 3 / 2)
|
|
|
|
|
|
|
|
|
|
System.arraycopy(nv12, 0, nv21, 0, frameSize)
|
|
|
|
|
|
|
|
|
|
var i = 0
|
|
|
|
|
while (i < frameSize / 2 - 1) {
|
|
|
|
|
nv21[frameSize + i] = nv12[frameSize + i + 1]
|
|
|
|
|
nv21[frameSize + i + 1] = nv12[frameSize + i]
|
|
|
|
|
i += 2
|
|
|
|
|
}
|
|
|
|
|
return nv21
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun i420ToNv21(i420: ByteArray, width: Int, height: Int): ByteArray {
|
|
|
|
|
val frameSize = width * height
|
|
|
|
|
val qFrameSize = frameSize / 4
|
|
|
|
|
val nv21 = ByteArray(frameSize * 3 / 2)
|
|
|
|
|
|
|
|
|
|
System.arraycopy(i420, 0, nv21, 0, frameSize)
|
|
|
|
|
|
|
|
|
|
val u = frameSize
|
|
|
|
|
val v = frameSize + qFrameSize
|
|
|
|
|
|
|
|
|
|
for (i in 0 until qFrameSize) {
|
|
|
|
|
nv21[frameSize + i * 2] = i420[v + i]
|
|
|
|
|
nv21[frameSize + i * 2 + 1] = i420[u + i]
|
|
|
|
|
}
|
|
|
|
|
return nv21
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class RenderLooper(
|
|
|
|
|
private val context: Context,
|
|
|
|
|
private val holder: SurfaceHolder,
|
|
|
|
|
private var screenWidth: Int,
|
|
|
|
|
private var screenHeight: Int,
|
|
|
|
|
handlerThread: HandlerThread
|
|
|
|
|
) {
|
|
|
|
|
private val handler = Handler(handlerThread.looper)
|
|
|
|
|
@Volatile
|
|
|
|
|
private var running = false
|
|
|
|
|
|
|
|
|
|
private var renderer: Renderer? = null
|
|
|
|
|
private var mediaFiles: Array<File>? = null
|
|
|
|
|
|
|
|
|
|
private val frameDelayMs = 1000L / 33L // 약 60fps
|
|
|
|
|
|
|
|
|
|
private val renderRunnable = object : Runnable {
|
|
|
|
|
override fun run() {
|
|
|
|
|
if (!running) return
|
|
|
|
|
val canvas = holder.lockCanvas()
|
|
|
|
|
val currentRenderer = renderer
|
|
|
|
|
if (canvas != null && currentRenderer != null) {
|
|
|
|
|
synchronized(currentRenderer) {
|
|
|
|
|
currentRenderer.render(canvas)
|
|
|
|
|
}
|
|
|
|
|
holder.unlockCanvasAndPost(canvas)
|
|
|
|
|
}
|
|
|
|
|
renderer?.update()
|
|
|
|
|
if (renderer?.isCycleCompleted() == true) {
|
|
|
|
|
loadNextMedia()
|
|
|
|
|
handler.postDelayed(this, frameDelayMs)
|
|
|
|
|
}
|
|
|
|
|
handler.postDelayed(this, frameDelayMs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun start() {
|
|
|
|
|
running = true
|
|
|
|
|
initMediaFiles()
|
|
|
|
|
loadNextMedia()
|
|
|
|
|
handler.post(renderRunnable)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun stop() {
|
|
|
|
|
running = false
|
|
|
|
|
handler.removeCallbacks(renderRunnable)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun initMediaFiles() {
|
|
|
|
|
val youtubeDLDir = File(
|
|
|
|
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
|
|
|
|
"wallPapers"
|
|
|
|
|
)
|
|
|
|
|
if (youtubeDLDir.exists() && youtubeDLDir.isDirectory && youtubeDLDir.listFiles().size > 2) {
|
|
|
|
|
val supportedImageExtensions = listOf("jpg", "jpeg", "png", "bmp", "webp")
|
|
|
|
|
val supportedVideoExtensions = listOf("mp4", "mkv", "avi")
|
|
|
|
|
mediaFiles = youtubeDLDir.listFiles { file ->
|
|
|
|
|
val ext = file.extension.lowercase()
|
|
|
|
|
ext in supportedImageExtensions || ext in supportedVideoExtensions
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Blog.LOGE("mediaFiles >>> ${mediaFiles?.size}")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun loadNextMedia() {
|
|
|
|
|
renderer?.release()
|
|
|
|
|
if (mediaFiles == null || mediaFiles!!.isEmpty()) {
|
|
|
|
|
prepareRawVideo()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mediaFiles?.random()?.let {
|
|
|
|
|
loadMedia(it)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun loadMedia(file: File) {
|
|
|
|
|
val ext = file.extension.lowercase()
|
|
|
|
|
renderer?.resetOffset()
|
|
|
|
|
|
|
|
|
|
if (ext in listOf("jpg", "jpeg", "png", "bmp", "webp")) {
|
|
|
|
|
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
|
|
|
|
renderer = ImageRenderer(context, bitmap, screenWidth, screenHeight)
|
|
|
|
|
} else {
|
|
|
|
|
try {
|
|
|
|
|
val extractor = MediaExtractor()
|
|
|
|
|
extractor.setDataSource(file.absolutePath)
|
|
|
|
|
renderer = VideoRenderer(context, extractor, screenWidth, screenHeight)
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
e.printStackTrace()
|
|
|
|
|
prepareRawVideo()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun updateScreenSize(width: Int, height: Int) {
|
|
|
|
|
screenWidth = width
|
|
|
|
|
screenHeight = height
|
|
|
|
|
renderer?.updateScreenSize(width, height)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun prepareRawVideo() {
|
|
|
|
|
try {
|
|
|
|
|
val extractor = MediaExtractor()
|
|
|
|
|
val afd = context.resources.openRawResourceFd(R.raw.sample)
|
|
|
|
|
extractor.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
|
|
|
|
|
afd.close()
|
|
|
|
|
renderer = VideoRenderer(context, extractor, screenWidth, screenHeight)
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
e.printStackTrace()
|
|
|
|
|
maxOffset = if (moveXAxis) diffWidth else if (moveYAxis) diffHeight else 0f
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|