...
This commit is contained in:
parent
fa5e6bbedc
commit
5ded732345
@ -414,7 +414,10 @@ void PlayerEngine::readThreadLoop() {
|
|||||||
|
|
||||||
// 4. 코덱 내부에 남아있는 옛날 프레임 찌꺼기 초기화
|
// 4. 코덱 내부에 남아있는 옛날 프레임 찌꺼기 초기화
|
||||||
// if (video_codec_ctx_) avcodec_flush_buffers(video_codec_ctx_);
|
// if (video_codec_ctx_) avcodec_flush_buffers(video_codec_ctx_);
|
||||||
|
// videoCodecFlushReq_ = false;
|
||||||
// if (audio_codec_ctx_) avcodec_flush_buffers(audio_codec_ctx_);
|
// if (audio_codec_ctx_) avcodec_flush_buffers(audio_codec_ctx_);
|
||||||
|
// // 이미 비웠으므로 false
|
||||||
|
// audioCodecFlushReq_ = false;
|
||||||
// if (sonic_stream_) {
|
// if (sonic_stream_) {
|
||||||
// sonicFlushStream(sonic_stream_);
|
// sonicFlushStream(sonic_stream_);
|
||||||
// }
|
// }
|
||||||
@ -541,6 +544,7 @@ void PlayerEngine::audioThreadLoop() {
|
|||||||
|
|
||||||
while (framesLeft > 0 && !abortRequest_) {
|
while (framesLeft > 0 && !abortRequest_) {
|
||||||
int32_t framesWritten = AAudioStream_write(audio_stream_, currentAAudioBuf, framesLeft, 1000000000);
|
int32_t framesWritten = AAudioStream_write(audio_stream_, currentAAudioBuf, framesLeft, 1000000000);
|
||||||
|
if (seekReq_) break;
|
||||||
if (framesWritten < 0) break;
|
if (framesWritten < 0) break;
|
||||||
framesLeft -= framesWritten;
|
framesLeft -= framesWritten;
|
||||||
currentAAudioBuf += framesWritten * 2; // 2채널 전진
|
currentAAudioBuf += framesWritten * 2; // 2채널 전진
|
||||||
|
|||||||
@ -251,150 +251,150 @@ class AggregatedNewsWorker(private val context: Context, params: WorkerParameter
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WallContentsWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
//class WallContentsWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
|
//
|
||||||
// 1. 필요한 Gemini 응답 데이터 클래스 추가
|
// // 1. 필요한 Gemini 응답 데이터 클래스 추가
|
||||||
data class GeminiResponse(val candidates: List<Candidate>)
|
// data class GeminiResponse(val candidates: List<Candidate>)
|
||||||
data class Candidate(val content: Content)
|
// data class Candidate(val content: Content)
|
||||||
data class Content(val parts: List<Part>)
|
// data class Content(val parts: List<Part>)
|
||||||
data class Part(val text: String)
|
// data class Part(val text: String)
|
||||||
|
//
|
||||||
// 불필요한 최상단 따옴표를 제거한 깔끔한 프롬프트
|
// // 불필요한 최상단 따옴표를 제거한 깔끔한 프롬프트
|
||||||
|
//
|
||||||
|
//
|
||||||
override suspend fun doWork(): Result {
|
// override suspend fun doWork(): Result {
|
||||||
return try {
|
// return try {
|
||||||
refreshGeminiContent()
|
// refreshGeminiContent()
|
||||||
Result.success()
|
// Result.success()
|
||||||
} catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
Blog.LOGE("WallContentsWorker Failed", e)
|
// Blog.LOGE("WallContentsWorker Failed", e)
|
||||||
Result.retry() // 일시적 오류일 수 있으므로 재시도 반환
|
// Result.retry() // 일시적 오류일 수 있으므로 재시도 반환
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private fun getTimeBasedContext(): String {
|
// private fun getTimeBasedContext(): String {
|
||||||
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
// val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
|
||||||
|
//
|
||||||
return when (hour) {
|
// return when (hour) {
|
||||||
in 6..10 -> "지금은 아침 시간이야. 카페에서 커피/브런치를 주문하거나, 상쾌하게 아침 인사를 나누는 상황에 맞는 가벼운 스몰토크를 알려줘."
|
// in 6..10 -> "지금은 아침 시간이야. 카페에서 커피/브런치를 주문하거나, 상쾌하게 아침 인사를 나누는 상황에 맞는 가벼운 스몰토크를 알려줘."
|
||||||
in 11..16 -> "지금은 낮 시간이야. 관광지에서 길을 묻거나, 점심 식사 주문, 상점에서 물건을 구매할 때 쓰는 활동적인 실생활 표현을 알려줘."
|
// in 11..16 -> "지금은 낮 시간이야. 관광지에서 길을 묻거나, 점심 식사 주문, 상점에서 물건을 구매할 때 쓰는 활동적인 실생활 표현을 알려줘."
|
||||||
in 17..21 -> "지금은 저녁 시간이야. 분위기 있는 식당에서 저녁을 먹거나, 펍에서 맥주 한잔하며 현지인과 가볍게 나누는 스몰토크를 알려줘."
|
// in 17..21 -> "지금은 저녁 시간이야. 분위기 있는 식당에서 저녁을 먹거나, 펍에서 맥주 한잔하며 현지인과 가볍게 나누는 스몰토크를 알려줘."
|
||||||
else -> "지금은 늦은 밤이야. 숙소(호텔) 프론트에 문의하거나, 편의점에서 야식을 사고, 내일 일정을 묻는 등 밤 시간에 필요한 실용적인 표현을 알려줘."
|
// else -> "지금은 늦은 밤이야. 숙소(호텔) 프론트에 문의하거나, 편의점에서 야식을 사고, 내일 일정을 묻는 등 밤 시간에 필요한 실용적인 표현을 알려줘."
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private suspend fun refreshGeminiContent() {
|
// private suspend fun refreshGeminiContent() {
|
||||||
val apiKey = PrefString.geminiApiKey.get("") // 설정에서 가져온 키
|
// val apiKey = PrefString.geminiApiKey.get("") // 설정에서 가져온 키
|
||||||
if (apiKey.isEmpty()) {
|
// if (apiKey.isEmpty()) {
|
||||||
Blog.LOGE("Gemini API Key is empty.")
|
// Blog.LOGE("Gemini API Key is empty.")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
val resultJson = callGeminiApi(apiKey, getDynamicPrompt()) ?: return
|
// val resultJson = callGeminiApi(apiKey, getDynamicPrompt()) ?: return
|
||||||
Blog.LOGE("resultJson >>> $resultJson")
|
// Blog.LOGE("resultJson >>> $resultJson")
|
||||||
val newGroups = parseGeminiResponse(resultJson) ?: return
|
// val newGroups = parseGeminiResponse(resultJson) ?: return
|
||||||
Blog.LOGE("newGroups.size ${newGroups.size}")
|
// Blog.LOGE("newGroups.size ${newGroups.size}")
|
||||||
WorkersDb.insertExpressions(newGroups)
|
// WorkersDb.insertExpressions(newGroups)
|
||||||
|
//
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private fun getDynamicPrompt(): String {
|
// private fun getDynamicPrompt(): String {
|
||||||
val myStyle = PrefString.travelStyle.get("주로 현지 로컬 맛집을 찾아다니고, 가벼운 스몰토크를 즐기는 편이야. 흡연 및 음주를 즐기는 편이고, 여행은 항상 부인과 같이 다녀")
|
// val myStyle = PrefString.travelStyle.get("주로 현지 로컬 맛집을 찾아다니고, 가벼운 스몰토크를 즐기는 편이야. 흡연 및 음주를 즐기는 편이고, 여행은 항상 부인과 같이 다녀")
|
||||||
val timeContext = getTimeBasedContext()
|
// val timeContext = getTimeBasedContext()
|
||||||
|
//
|
||||||
return """
|
// return """
|
||||||
너는 여행 및 외국어 학습 전문가야. 아래의 [나의 스타일]과 [현재 상황]을 반영해서, 당장 써먹을 수 있는 유용한 실용 회화 표현 12가지를 선정해줘.
|
// 너는 여행 및 외국어 학습 전문가야. 아래의 [나의 스타일]과 [현재 상황]을 반영해서, 당장 써먹을 수 있는 유용한 실용 회화 표현 12가지를 선정해줘.
|
||||||
|
//
|
||||||
[나의 스타일]: $myStyle
|
// [나의 스타일]: $myStyle
|
||||||
[현재 상황]: $timeContext
|
// [현재 상황]: $timeContext
|
||||||
|
//
|
||||||
각 표현에 대해 영어(EN), 스페인어(ES), 일본어(JA), 중국어(ZH) 네 가지 버전을 제공해.
|
// 각 표현에 대해 영어(EN), 스페인어(ES), 일본어(JA), 중국어(ZH) 네 가지 버전을 제공해.
|
||||||
|
//
|
||||||
중요 규칙:
|
// 중요 규칙:
|
||||||
- 모든 외국어 문장의 'pron'에는 최대한 정확한 한국어 발음을 적어줘.
|
// - 모든 외국어 문장의 'pron'에는 최대한 정확한 한국어 발음을 적어줘.
|
||||||
- 일본어(JA)는 한자와 가나 혼용, 중국어(ZH)는 간체자를 쓰고 'pron'에 병음과 한글 발음을 적어줘.
|
// - 일본어(JA)는 한자와 가나 혼용, 중국어(ZH)는 간체자를 쓰고 'pron'에 병음과 한글 발음을 적어줘.
|
||||||
- 각 번역본마다 그 문장에서 가장 중요한 '핵심 단어나 숙어' 2~3개를 뽑아서 `words` 배열에 원단어(word)와 뜻(meaning)으로 정리해줘.
|
// - 각 번역본마다 그 문장에서 가장 중요한 '핵심 단어나 숙어' 2~3개를 뽑아서 `words` 배열에 원단어(word)와 뜻(meaning)으로 정리해줘.
|
||||||
|
//
|
||||||
반드시 아래 JSON 배열 형식으로만 응답해:
|
// 반드시 아래 JSON 배열 형식으로만 응답해:
|
||||||
[
|
// [
|
||||||
{
|
// {
|
||||||
"topic": "주제",
|
// "topic": "주제",
|
||||||
"translations": [
|
// "translations": [
|
||||||
{
|
// {
|
||||||
"lang": "EN",
|
// "lang": "EN",
|
||||||
"main": "Sentence",
|
// "main": "Sentence",
|
||||||
"sub": "한국어 뜻",
|
// "sub": "한국어 뜻",
|
||||||
"pron": "한글 발음",
|
// "pron": "한글 발음",
|
||||||
"words": [
|
// "words": [
|
||||||
{"word": "단어1", "meaning": "뜻1", "pron": "단어1 발음"},
|
// {"word": "단어1", "meaning": "뜻1", "pron": "단어1 발음"},
|
||||||
{"word": "단어2", "meaning": "뜻2", "pron": "단어2 발음"}
|
// {"word": "단어2", "meaning": "뜻2", "pron": "단어2 발음"}
|
||||||
]
|
// ]
|
||||||
},
|
// },
|
||||||
... (ES, JA, ZH도 동일한 구조)
|
// ... (ES, JA, ZH도 동일한 구조)
|
||||||
]
|
// ]
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
// (이 형식으로 총 12개 세트를 제공할 것)
|
// // (이 형식으로 총 12개 세트를 제공할 것)
|
||||||
""".trimIndent()
|
// """.trimIndent()
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private suspend fun callGeminiApi(apiKey: String, prompt: String): String? {
|
// private suspend fun callGeminiApi(apiKey: String, prompt: String): String? {
|
||||||
return withContext(Dispatchers.IO) {
|
// return withContext(Dispatchers.IO) {
|
||||||
val client = OkHttpClient.Builder()
|
// val client = OkHttpClient.Builder()
|
||||||
.connectTimeout(60, TimeUnit.SECONDS) // 서버 연결 대기 시간
|
// .connectTimeout(60, TimeUnit.SECONDS) // 서버 연결 대기 시간
|
||||||
.readTimeout(60, TimeUnit.SECONDS) // 데이터 전달받는 시간 (중요!)
|
// .readTimeout(60, TimeUnit.SECONDS) // 데이터 전달받는 시간 (중요!)
|
||||||
.writeTimeout(60, TimeUnit.SECONDS) // 데이터 전송 시간
|
// .writeTimeout(60, TimeUnit.SECONDS) // 데이터 전송 시간
|
||||||
.build()
|
// .build()
|
||||||
val url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=$apiKey"
|
// val url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=$apiKey"
|
||||||
|
//
|
||||||
// 3. Gson을 사용하여 JSON 구조를 안전하게 생성 (따옴표 깨짐 방지)
|
// // 3. Gson을 사용하여 JSON 구조를 안전하게 생성 (따옴표 깨짐 방지)
|
||||||
val requestMap = mapOf(
|
// val requestMap = mapOf(
|
||||||
"contents" to listOf(
|
// "contents" to listOf(
|
||||||
mapOf("parts" to listOf(mapOf("text" to prompt)))
|
// mapOf("parts" to listOf(mapOf("text" to prompt)))
|
||||||
),
|
// ),
|
||||||
"generationConfig" to mapOf(
|
// "generationConfig" to mapOf(
|
||||||
"responseMimeType" to "application/json"
|
// "responseMimeType" to "application/json"
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
|
//
|
||||||
val jsonRequest = Gson().toJson(requestMap)
|
// val jsonRequest = Gson().toJson(requestMap)
|
||||||
|
//
|
||||||
val body = jsonRequest.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
// val body = jsonRequest.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||||
val request = Request.Builder()
|
// val request = Request.Builder()
|
||||||
.url(url)
|
// .url(url)
|
||||||
.post(body)
|
// .post(body)
|
||||||
.build()
|
// .build()
|
||||||
|
//
|
||||||
try {
|
// try {
|
||||||
val response = client.newCall(request).execute()
|
// val response = client.newCall(request).execute()
|
||||||
if (response.isSuccessful) {
|
// if (response.isSuccessful) {
|
||||||
response.body?.string()
|
// response.body?.string()
|
||||||
} else {
|
// } else {
|
||||||
Blog.LOGE("Gemini API Error: ${response.code} ${response.message}")
|
// Blog.LOGE("Gemini API Error: ${response.code} ${response.message}")
|
||||||
null
|
// null
|
||||||
}
|
// }
|
||||||
} catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
e.printStackTrace()
|
// e.printStackTrace()
|
||||||
null
|
// null
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private fun parseGeminiResponse(jsonString: String): List<WallContentGroup>? {
|
// private fun parseGeminiResponse(jsonString: String): List<WallContentGroup>? {
|
||||||
return try {
|
// return try {
|
||||||
val gson = Gson()
|
// val gson = Gson()
|
||||||
val fullResponse = gson.fromJson(jsonString, GeminiResponse::class.java)
|
// val fullResponse = gson.fromJson(jsonString, GeminiResponse::class.java)
|
||||||
val innerJson = fullResponse.candidates[0].content.parts[0].text
|
// val innerJson = fullResponse.candidates[0].content.parts[0].text
|
||||||
|
//
|
||||||
val groupType = object : TypeToken<List<WallContentGroup>>() {}.type
|
// val groupType = object : TypeToken<List<WallContentGroup>>() {}.type
|
||||||
gson.fromJson(innerJson, groupType)
|
// gson.fromJson(innerJson, groupType)
|
||||||
} catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
Blog.LOGE("Parsing Error: ${e.message}")
|
// Blog.LOGE("Parsing Error: ${e.message}")
|
||||||
null
|
// null
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
}
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -718,22 +718,6 @@ class ForeGroundService : Service() {
|
|||||||
workManager.enqueueUniquePeriodicWork("AggregatedNewsWork", ExistingPeriodicWorkPolicy.KEEP, newsRequest)
|
workManager.enqueueUniquePeriodicWork("AggregatedNewsWork", ExistingPeriodicWorkPolicy.KEEP, newsRequest)
|
||||||
|
|
||||||
|
|
||||||
// val wallpaperRequest = PeriodicWorkRequestBuilder<WallContentsWorker>(
|
|
||||||
// 2, TimeUnit.HOURS // 1시간 간격 설정
|
|
||||||
// )
|
|
||||||
// .setConstraints(
|
|
||||||
// Constraints.Builder()
|
|
||||||
// .setRequiredNetworkType(NetworkType.CONNECTED) // 네트워크 연결 시에만
|
|
||||||
// .build()
|
|
||||||
// )
|
|
||||||
// .build()
|
|
||||||
|
|
||||||
// workManager.enqueueUniquePeriodicWork(
|
|
||||||
// "WallContentsWork",
|
|
||||||
// ExistingPeriodicWorkPolicy.KEEP,
|
|
||||||
// wallpaperRequest
|
|
||||||
// )
|
|
||||||
|
|
||||||
val quoteRequest = PeriodicWorkRequestBuilder<QuoteFetchWorker>(
|
val quoteRequest = PeriodicWorkRequestBuilder<QuoteFetchWorker>(
|
||||||
2, TimeUnit.HOURS // 1시간 간격 설정
|
2, TimeUnit.HOURS // 1시간 간격 설정
|
||||||
)
|
)
|
||||||
|
|||||||
@ -56,6 +56,51 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
|
|||||||
private var subtitleSyncJob: Job? = null
|
private var subtitleSyncJob: Job? = null
|
||||||
private val allSubtitleTracks = mutableListOf<SubtitleTrack>()
|
private val allSubtitleTracks = mutableListOf<SubtitleTrack>()
|
||||||
|
|
||||||
|
private var lastCheckedPos = -1.0
|
||||||
|
private var lastCheckedTime = 0L
|
||||||
|
private var isRecovering = false
|
||||||
|
|
||||||
|
private fun rebootEngine(resumePos: Double) {
|
||||||
|
isRecovering = true
|
||||||
|
Toast.makeText(this, "재생 환경을 복구하는 중입니다...", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
// 1. 기존 엔진 중지
|
||||||
|
nativePlayer?.stop()
|
||||||
|
|
||||||
|
// 2. 다시 준비 (기존 경로 및 surface 활용)
|
||||||
|
// onPreparedListener에서 resumePos로 다시 seekTo 하도록 설정
|
||||||
|
nativePlayer?.onPreparedListener = {
|
||||||
|
nativePlayer?.seekTo(resumePos)
|
||||||
|
nativePlayer?.play(Surface(videoTextureView.surfaceTexture!!))
|
||||||
|
isRecovering = false
|
||||||
|
lastCheckedTime = System.currentTimeMillis() // 타임스탬프 갱신
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이전에 사용했던 FD를 다시 넘기거나 경로를 통해 다시 Open
|
||||||
|
prepareEngine()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareEngine() {
|
||||||
|
val videoFile = File(videoPath)
|
||||||
|
if (videoFile.exists()) {
|
||||||
|
val videoPfd = ParcelFileDescriptor.open(videoFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||||
|
val videoFd = videoPfd.detachFd()
|
||||||
|
|
||||||
|
var subFd = -1
|
||||||
|
if (subtitlePath.isNotEmpty()) {
|
||||||
|
val subFile = File(subtitlePath)
|
||||||
|
if (subFile.exists()) {
|
||||||
|
val subPfd = ParcelFileDescriptor.open(subFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||||
|
subFd = subPfd.detachFd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C++로 DataSource를 넘기고 비동기 Prepare 명령!
|
||||||
|
nativePlayer?.setDataSource(videoFd, subFd)
|
||||||
|
nativePlayer?.prepareAsync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
@ -93,12 +138,10 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
|
|||||||
onPreparedListener = {
|
onPreparedListener = {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
loadAvailableSubtitles()
|
loadAvailableSubtitles()
|
||||||
startUIUpdateLoop()
|
|
||||||
if (allSubtitleTracks.size > 1) {
|
if (allSubtitleTracks.size > 1) {
|
||||||
showSubtitleSelectionDialog()
|
showSubtitleSelectionDialog()
|
||||||
} else {
|
} else {
|
||||||
// 자막 없으면 자동 시작
|
play()
|
||||||
play(Surface(videoTextureView.surfaceTexture!!))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,30 +274,32 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
|
|||||||
tvTime.text = "${formatTime(currentPos)} / ${formatTime(duration)}"
|
tvTime.text = "${formatTime(currentPos)} / ${formatTime(duration)}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val currentPos = nativePlayer?.getCurrentPosition() ?: 0.0
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
Log.e("PlayerWatchdog", "isPlaying $isPlaying isUserSeeking $isUserSeeking isRecovering $isRecovering" )
|
||||||
|
if (isPlaying && !isUserSeeking && !isRecovering) {
|
||||||
|
// 재생 위치가 변했는지 확인
|
||||||
|
if (currentPos != lastCheckedPos) {
|
||||||
|
lastCheckedPos = currentPos
|
||||||
|
lastCheckedTime = now
|
||||||
|
} else {
|
||||||
|
// 위치가 그대로라면, 얼마나 오래 멈춰있었는지 확인 (3초 이상)
|
||||||
|
if (now - lastCheckedTime > 3000) {
|
||||||
|
Log.e("PlayerWatchdog", "영상이 멈춘 것으로 판단됨! 복구 시작...")
|
||||||
|
rebootEngine(currentPos)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delay(500) // 0.5초마다 UI 갱신
|
delay(500) // 0.5초마다 UI 갱신
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) {
|
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) {
|
||||||
val videoFile = File(videoPath)
|
prepareEngine()
|
||||||
if (videoFile.exists()) {
|
|
||||||
val videoPfd = ParcelFileDescriptor.open(videoFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
||||||
val videoFd = videoPfd.detachFd()
|
|
||||||
|
|
||||||
var subFd = -1
|
|
||||||
if (subtitlePath.isNotEmpty()) {
|
|
||||||
val subFile = File(subtitlePath)
|
|
||||||
if (subFile.exists()) {
|
|
||||||
val subPfd = ParcelFileDescriptor.open(subFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
||||||
subFd = subPfd.detachFd()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// C++로 DataSource를 넘기고 비동기 Prepare 명령!
|
|
||||||
nativePlayer?.setDataSource(videoFd, subFd)
|
|
||||||
nativePlayer?.prepareAsync()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadAvailableSubtitles() {
|
private fun loadAvailableSubtitles() {
|
||||||
@ -276,11 +321,16 @@ class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
|
|||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setItems(trackNames) { _, which ->
|
.setItems(trackNames) { _, which ->
|
||||||
selectSubtitleTrack(allSubtitleTracks[which])
|
selectSubtitleTrack(allSubtitleTracks[which])
|
||||||
nativePlayer?.play(Surface(videoTextureView.surfaceTexture!!))
|
play()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun play() {
|
||||||
|
startUIUpdateLoop()
|
||||||
|
nativePlayer?.play(Surface(videoTextureView.surfaceTexture!!))
|
||||||
|
}
|
||||||
|
|
||||||
private fun selectSubtitleTrack(track: SubtitleTrack) {
|
private fun selectSubtitleTrack(track: SubtitleTrack) {
|
||||||
if (track.isExternal) {
|
if (track.isExternal) {
|
||||||
nativePlayer?.setInternalSubtitleTrack(-1)
|
nativePlayer?.setInternalSubtitleTrack(-1)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user