@@ -10,25 +10,32 @@ import com.ismartcoding.lib.logcat.LogCat
1010import com.ismartcoding.lib.upnp.UPnPController
1111import com.ismartcoding.lib.upnp.UPnPDevice
1212import com.ismartcoding.lib.upnp.UPnPDiscovery
13+ import com.ismartcoding.plain.data.IMedia
1314import com.ismartcoding.plain.features.media.CastPlayer
1415import com.ismartcoding.plain.helpers.UrlHelper
16+ import com.ismartcoding.plain.ui.helpers.DialogHelper
1517import io.ktor.client.HttpClient
1618import io.ktor.client.call.body
1719import io.ktor.client.engine.cio.CIO
1820import io.ktor.client.request.get
1921import io.ktor.http.HttpStatusCode
2022import kotlinx.coroutines.Dispatchers
23+ import kotlinx.coroutines.delay
2124import kotlinx.coroutines.flow.MutableStateFlow
2225import kotlinx.coroutines.flow.StateFlow
2326import kotlinx.coroutines.flow.buffer
2427import kotlinx.coroutines.flow.flowOn
2528import kotlinx.coroutines.launch
29+ import kotlinx.coroutines.Job
2630
2731class CastViewModel : ViewModel () {
2832 private val _itemsFlow = MutableStateFlow (mutableStateListOf<UPnPDevice >())
2933 val itemsFlow: StateFlow <List <UPnPDevice >> get() = _itemsFlow
3034 var castMode = mutableStateOf(false )
3135 var showCastDialog = mutableStateOf(false )
36+ val isLoading = mutableStateOf(false )
37+
38+ private var positionUpdateJob: Job ? = null
3239
3340 fun enterCastMode () {
3441 castMode.value = true
@@ -44,17 +51,110 @@ class CastViewModel : ViewModel() {
4451 val device = CastPlayer .currentDevice ? : return
4552 viewModelScope.launch(Dispatchers .IO ) {
4653 UPnPController .stopAVTransportAsync(device)
54+ CastPlayer .isPlaying.value = false
55+
56+ // 清理投屏状态
57+ if (CastPlayer .sid.isNotEmpty()) {
58+ UPnPController .unsubscribeEvent(device, CastPlayer .sid)
59+ CastPlayer .sid = " "
60+ }
61+ CastPlayer .supportsCallback.value = false
62+ CastPlayer .progress.value = 0f
63+ CastPlayer .duration.value = 0f
64+
65+ // 取消位置更新作业
66+ positionUpdateJob?.cancel()
67+ positionUpdateJob = null
4768 }
4869 }
4970
5071 fun cast (path : String ) {
5172 val device = CastPlayer .currentDevice ? : return
5273 viewModelScope.launch(Dispatchers .IO ) {
53- CastPlayer .currentUri = path
54- UPnPController .setAVTransportURIAsync(device, UrlHelper .getMediaHttpUrl(path))
55- if (CastPlayer .sid.isNotEmpty()) {
56- UPnPController .unsubscribeEvent(device, CastPlayer .sid)
57- CastPlayer .sid = " "
74+ isLoading.value = true
75+ CastPlayer .setCurrentUri(path)
76+ try {
77+ UPnPController .setAVTransportURIAsync(device, UrlHelper .getMediaHttpUrl(path))
78+ CastPlayer .isPlaying.value = true
79+ if (CastPlayer .sid.isNotEmpty()) {
80+ UPnPController .unsubscribeEvent(device, CastPlayer .sid)
81+ CastPlayer .sid = " "
82+ }
83+
84+ // 尝试订阅事件回调
85+ trySubscribeEvent()
86+ } catch (e: Exception ) {
87+ DialogHelper .showErrorMessage(e.message ? : " Cast failed" )
88+ } finally {
89+ isLoading.value = false
90+ }
91+ }
92+ }
93+
94+ fun cast (item : IMedia ) {
95+ val device = CastPlayer .currentDevice ? : return
96+ viewModelScope.launch(Dispatchers .IO ) {
97+ CastPlayer .setCurrentUri(item.path)
98+ isLoading.value = true
99+ val castItems = CastPlayer .items.value
100+ val isInQueue = castItems.any { it.path == item.path }
101+ if (! isInQueue) {
102+ CastPlayer .addItem(item)
103+ }
104+ try {
105+ UPnPController .setAVTransportURIAsync(device, UrlHelper .getMediaHttpUrl(item.path))
106+ CastPlayer .isPlaying.value = true
107+ if (CastPlayer .sid.isNotEmpty()) {
108+ UPnPController .unsubscribeEvent(device, CastPlayer .sid)
109+ CastPlayer .sid = " "
110+ }
111+
112+ // 尝试订阅事件回调
113+ trySubscribeEvent()
114+ } catch (e: Exception ) {
115+ DialogHelper .showErrorMessage(e.message ? : " Cast failed" )
116+ } finally {
117+ isLoading.value = false
118+ }
119+ }
120+ }
121+
122+ private suspend fun trySubscribeEvent () {
123+ val device = CastPlayer .currentDevice ? : return
124+ try {
125+ val sid = UPnPController .subscribeEvent(device, UrlHelper .getCastCallbackUrl())
126+ if (sid.isNotEmpty()) {
127+ CastPlayer .sid = sid
128+ CastPlayer .supportsCallback.value = true
129+ // 开始定期获取播放位置
130+ startPositionUpdater()
131+ } else {
132+ CastPlayer .supportsCallback.value = false
133+ }
134+ } catch (e: Exception ) {
135+ CastPlayer .supportsCallback.value = false
136+ }
137+ }
138+
139+ private fun startPositionUpdater () {
140+ val device = CastPlayer .currentDevice ? : return
141+ if (! CastPlayer .supportsCallback.value) return
142+
143+ // 取消之前的作业
144+ positionUpdateJob?.cancel()
145+
146+ positionUpdateJob = viewModelScope.launch(Dispatchers .IO ) {
147+ while (CastPlayer .currentUri.value.isNotEmpty() && CastPlayer .supportsCallback.value) {
148+ try {
149+ if (CastPlayer .isPlaying.value) {
150+ val positionInfo = UPnPController .getPositionInfoAsync(device)
151+ CastPlayer .updatePositionInfo(positionInfo.relTime, positionInfo.trackDuration)
152+ }
153+ } catch (e: Exception ) {
154+ // 获取位置信息失败,可能不支持
155+ break
156+ }
157+ delay(1000 ) // 每秒更新一次
58158 }
59159 }
60160 }
@@ -84,4 +184,20 @@ class CastViewModel : ViewModel() {
84184 _itemsFlow .value.add(device)
85185 }
86186 }
187+
188+ fun playCast () {
189+ val device = CastPlayer .currentDevice ? : return
190+ viewModelScope.launch(Dispatchers .IO ) {
191+ UPnPController .playAVTransportAsync(device)
192+ CastPlayer .isPlaying.value = true
193+ }
194+ }
195+
196+ fun pauseCast () {
197+ val device = CastPlayer .currentDevice ? : return
198+ viewModelScope.launch(Dispatchers .IO ) {
199+ UPnPController .pauseAVTransportAsync(device)
200+ CastPlayer .isPlaying.value = false
201+ }
202+ }
87203}
0 commit comments