This commit is contained in:
2026-04-11 17:32:14 +09:00
parent 2327326cca
commit d0422fafeb
14 changed files with 2297 additions and 130 deletions
@@ -951,16 +951,26 @@ open class LauncherActivity : CommonActivity() {
private val messageListener = MessageClient.OnMessageReceivedListener { messageEvent ->
when (messageEvent.path) {
"/gesture/right",
"/gesture/next" -> {
// 시계에서 오른쪽으로 휘둘렀을 때: 예) 다음 배경화면으로 변경
}
"/gesture/left",
"/gesture/prev" -> {
onSwipeRight()
// 시계에서 왼쪽으로 휘둘렀을 때: 예) 앱 서랍 열기
// showToast("시계 제스처: 앱 서랍")
}
"/gesture/down" ->{
onSwipeDown()
}
"/gesture/up"->{
val intent = Intent("ACTION_NEXT_WALLPAPER").setPackage(packageName)
sendBroadcast(intent)
showToast("시계 제스처: 다음 배경화면")
}
"/gesture/prev" -> {
// 시계에서 왼쪽으로 휘둘렀을 때: 예) 앱 서랍 열기
showAppDrawer()
showToast("시계 제스처: 앱 서랍")
// showAppDrawer()
}
}
}
@@ -168,6 +168,8 @@ class CompletedFilesFragment : Fragment() {
loadFiles()
} else {
if (extVideos.contains(file.extension.lowercase())) {
trackFileAccess(file.name)
loadFiles()
val intent = Intent(requireContext(), PlayerActivity::class.java).apply {
putExtra("VIDEO_PATH", file.absolutePath)
}
@@ -152,7 +152,9 @@ open class NeoRssActivity : CommonActivity() {
//// showContents(R.id.close)
// }
// }
when (messageEvent.path) {
"/gesture/right",
"/gesture/next" -> {
when(currentFragment) {
is RssHome ->{
@@ -173,9 +175,12 @@ open class NeoRssActivity : CommonActivity() {
}
}
}
"/gesture/left",
"/gesture/prev" -> {
currentFragment.onRemoteLeft(false)
}
"/gesture/up" ->{}
"/gesture/down" ->{}
}
}
@@ -24,6 +24,19 @@ class NativePlayer {
fun seekBy(sec: Double) = nativeSeekBy(nativeHandle, sec)
fun setSpeed(speed: Float) = nativeSetSpeed(nativeHandle, speed)
// 외부 함수 호출 정의
fun getDuration(): Double {
return if (nativeHandle != 0L) nativeGetDuration(nativeHandle) else 0.0
}
fun seekTo(sec: Double) {
if (nativeHandle != 0L) nativeSeekTo(nativeHandle, sec)
}
// 하단에 external fun 추가
private external fun nativeGetDuration(h: Long): Double
private external fun nativeSeekTo(h: Long, s: Double)
@Suppress("unused")
private fun onSubtitleTextDecoded(text: String) {
subtitleCallback?.invoke(text)
@@ -93,6 +93,7 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
onPreparedListener = {
runOnUiThread {
loadAvailableSubtitles()
startUIUpdateLoop()
if (allSubtitleTracks.size > 1) {
showSubtitleSelectionDialog()
} else {
@@ -114,6 +115,11 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
setupGestures()
}
private lateinit var seekBar: android.widget.SeekBar
private lateinit var tvTime: TextView
private var uiUpdateJob: Job? = null
private var isUserSeeking = false
private fun setupUI() {
val root = FrameLayout(this).apply { setBackgroundColor(Color.BLACK) }
@@ -156,11 +162,80 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
root.addView(gestureLayer)
root.addView(btnRotate, FrameLayout.LayoutParams(150, 150, Gravity.BOTTOM or Gravity.START).apply { setMargins(30,0,0,30) })
root.addView(btnHideVideo, FrameLayout.LayoutParams(150, 150, Gravity.BOTTOM or Gravity.END).apply { setMargins(0,0,30,30) })
val bottomControlLayout = android.widget.LinearLayout(this).apply {
orientation = android.widget.LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
setBackgroundColor(Color.parseColor("#99000000")) // 반투명 검은색 배경
setPadding(40, 20, 40, 20)
}
tvTime = TextView(this).apply {
setTextColor(Color.WHITE)
textSize = 16f
text = "00:00 / 00:00"
}
seekBar = android.widget.SeekBar(this).apply {
layoutParams = android.widget.LinearLayout.LayoutParams(0, -2, 1f).apply {
setMargins(30, 0, 30, 0)
}
setOnSeekBarChangeListener(object : android.widget.SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(bar: android.widget.SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
val duration = nativePlayer?.getDuration() ?: 0.0
tvTime.text = "${formatTime(progress.toDouble())} / ${formatTime(duration)}"
}
}
override fun onStartTrackingTouch(bar: android.widget.SeekBar?) {
isUserSeeking = true // 사용자가 잡고 있을 때는 자동 업데이트 중지
}
override fun onStopTrackingTouch(bar: android.widget.SeekBar?) {
isUserSeeking = false
val targetSec = bar?.progress?.toDouble() ?: 0.0
nativePlayer?.seekTo(targetSec) // 원하는 위치로 이동 요청
}
})
}
bottomControlLayout.addView(tvTime)
bottomControlLayout.addView(seekBar)
root.addView(bottomControlLayout, FrameLayout.LayoutParams(-1, -2, Gravity.BOTTOM))
setContentView(root)
hideSystemUI()
}
private fun formatTime(seconds: Double): String {
val totalSec = seconds.toInt()
val h = totalSec / 3600
val m = (totalSec % 3600) / 60
val s = totalSec % 60
return if (h > 0) String.format("%02d:%02d:%02d", h, m, s) else String.format("%02d:%02d", m, s)
}
// UI 갱신 루프 시작 함수 추가
private fun startUIUpdateLoop() {
uiUpdateJob?.cancel()
uiUpdateJob = CoroutineScope(Dispatchers.Main).launch {
while (isActive) {
if (isPlaying && !isUserSeeking) {
val currentPos = nativePlayer?.getCurrentPosition() ?: 0.0
val duration = nativePlayer?.getDuration() ?: 0.0
if (duration > 0) {
seekBar.max = duration.toInt()
seekBar.progress = currentPos.toInt()
tvTime.text = "${formatTime(currentPos)} / ${formatTime(duration)}"
}
}
delay(500) // 0.5초마다 UI 갱신
}
}
}
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) {
val videoFile = File(videoPath)
if (videoFile.exists()) {
@@ -232,7 +307,7 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
val rightDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
override fun onLongPress(e: MotionEvent) { nativePlayer?.setSpeed(4.0f) }
override fun onSingleTapUp(e: MotionEvent): Boolean {
nativePlayer?.seekBy(20.0)
nativePlayer?.seekBy(40.0)
return true
}
})
@@ -329,19 +404,31 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
private fun cleanSubtitleText(text: String): String = text.replace(Regex("\\{.*?\\}"), "")
var lastSubTitle : String = ""
// 💡 클래스 상단에 인덱스 기억용 변수 하나만 추가해 주세요.
private var currentSubtitleIndex = 0
private fun startSubtitleSyncLoop() {
subtitleSyncJob?.cancel()
// 💡 자막이 아예 없다면 작업을 시작조차 하지 않음
if (externalSubtitles.isEmpty()) {
subtitleView.visibility = View.INVISIBLE
return
}
// 💡 Dispatchers.Main 대신 Default에서 연산하고 UI만 Main에서 갱신하는 것이 버퍼링 방지에 좋습니다.
subtitleSyncJob = CoroutineScope(Dispatchers.Main).launch {
while (isActive) {
if (isPlaying) {
val currentSec = nativePlayer?.getCurrentPosition() ?: 0.0
val currentSub = externalSubtitles.find { currentSec in it.startSec..it.endSec }
// 💡 최적화된 인덱스 탐색 함수 호출
val currentSub = findSubtitleIndexed(currentSec)
if (currentSub != null) {
// 💡 번역본이 존재하면 [번역본] + [줄바꿈] + [원본] 형태로 보여주거나, 번역본만 보여줍니다.
val displayText = if (currentSub.translatedText != null) {
"${currentSub.translatedText}\n${cleanSubtitleText(currentSub.text)}"
// (원본이 보기 싫다면 그냥 currentSub.translatedText 만 넣으셔도 됩니다!)
} else {
cleanSubtitleText(currentSub.text)
}
@@ -355,11 +442,36 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
subtitleView.visibility = View.INVISIBLE
}
}
delay(100)
delay(200)
}
}
}
/**
* 💡 인덱스 기반 탐색: 이전 위치부터 찾기 때문에 CPU 부하가 거의 없습니다.
*/
private fun findSubtitleIndexed(currentSec: Double): SubtitleBlock? {
// 1. 영상이 뒤로 감기 되었을 경우 인덱스 초기화
if (currentSubtitleIndex >= externalSubtitles.size ||
externalSubtitles[currentSubtitleIndex].startSec > currentSec) {
currentSubtitleIndex = 0
}
// 2. 마지막으로 찾았던 위치(currentSubtitleIndex)부터 탐색 시작
for (i in currentSubtitleIndex until externalSubtitles.size) {
val item = externalSubtitles[i]
if (currentSec in item.startSec..item.endSec) {
currentSubtitleIndex = i // 현재 위치 저장
return item
}
// 3. 자막이 시간순으로 정렬되어 있다면, 현재 시간보다 시작 시간이 커지는 순간 루프 종료
if (item.startSec > currentSec) break
}
return null
}
private fun readTextWithEncoding(file: File): String {
val bytes = file.readBytes()
@@ -450,6 +562,7 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
} catch (e: Exception) {
Log.e("PlayerActivity", "Subtitle parsing error", e)
}
result.sortBy { it.startSec }
return result
}
@@ -561,5 +674,6 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
nativePlayer?.destroy()
leftLongPressJob?.cancel()
subtitleSyncJob?.cancel()
uiUpdateJob?.cancel()
}
}