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