package expo.modules.dlnaplayer import android.annotation.SuppressLint import android.annotation.TargetApi import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.net.wifi.WifiManager import android.os.Handler import android.os.Looper import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import org.json.JSONObject import java.net.InetAddress import java.net.URL import java.util.UUID import java.util.concurrent.ConcurrentHashMap import android.util.Log import expo.modules.kotlin.AppContext import java.io.BufferedReader import java.io.InputStreamReader import java.io.OutputStreamWriter import java.net.HttpURLConnection import java.util.concurrent.Executors // Miracast相关导入 import android.hardware.display.DisplayManager import android.media.MediaRouter import android.media.projection.MediaProjectionManager import android.content.Intent import android.os.Build import android.media.projection.MediaProjection import java.net.InetSocketAddress import android.net.Uri import androidx.core.net.toUri import org.json.JSONArray import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.net.DatagramPacket private const val TAG = "ExpoDlnaPlayerModule" // Miracast常量 private const val SCREEN_MIRRORING = "SCREEN_MIRRORING" private const val VIDEO_ONLY = "VIDEO_ONLY" class ExpoDlnaPlayerModule : Module() { private val discoveryManagerInstance by lazy { DiscoveryManager(appContext) } private val connectionManagerInstance by lazy { ConnectionManager() } private val deviceList = ConcurrentHashMap() private var currentDevice: JSONObject? = null private val executor = Executors.newCachedThreadPool() private val mainHandler = Handler(Looper.getMainLooper()) // Miracast相关 private val miracastManager by lazy { MiracastManager(appContext) } private var mediaProjection: MediaProjection? = null private var isProjecting = false private var projectionData: Intent? = null // 添加设备列表锁,确保线程安全 private val deviceListLock = Any() // 添加设备管理方法 private fun addOrUpdateDevice(device: JSONObject) { synchronized(deviceListLock) { val deviceId = device.optString("id") if (deviceId.isNotEmpty()) { // 检查controlURL是否存在 val controlURL = device.optString("controlURL", "") if (controlURL.isNotEmpty()) { Log.i(TAG, "设备(ID: $deviceId)拥有controlURL: $controlURL") } else { Log.i(TAG, "设备(ID: $deviceId)没有controlURL") } synchronized(deviceList) { val existingDeviceIndex = deviceList.values.indexOfFirst { it.optString("id") == deviceId } if (existingDeviceIndex >= 0) { // 更新现有设备 Log.i(TAG, "更新设备列表中的设备: $deviceId") deviceList[deviceId] = device } else { // 添加新设备 Log.i(TAG, "添加新设备到设备列表: $deviceId") deviceList[deviceId] = device } } // 确保发送到前端的设备信息包含controlURL val deviceMap = device.toMap().toMutableMap() if (controlURL.isNotEmpty() && !deviceMap.containsKey("controlURL")) { Log.i(TAG, "设备Map中缺少controlURL,手动添加它: $controlURL") deviceMap["controlURL"] = controlURL } // 发送设备发现事件给前端 Log.i(TAG, "发送onDeviceFound事件, 设备ID: $deviceId,包含controlURL: ${deviceMap.containsKey("controlURL")}") sendEvent("onDeviceFound", deviceMap) } else { Log.e(TAG, "设备缺少ID,无法添加到设备列表") } } } private fun removeDevice(deviceId: String) { synchronized(deviceListLock) { deviceList.remove(deviceId)?.let { Log.i(TAG, "设备已从列表移除: ${it.optString("name")} ($deviceId)") } } } private fun getDevice(deviceId: String): JSONObject? { synchronized(deviceListLock) { return deviceList[deviceId]?.also { Log.i(TAG, "获取设备信息: ${it.optString("name")} ($deviceId)") } ?: run { Log.e(TAG, "设备未找到: $deviceId") null } } } // 模块清理方法 - 用于替代finalize方法,在Kotlin中不应该重写finalize private fun cleanUp() { try { // 停止所有搜索 discoveryManagerInstance.stopDiscovery() miracastManager.stopDiscovery() // 停止状态更新任务 stopStatusUpdates() // 断开连接 if (connectionManagerInstance.isConnected()) { connectionManagerInstance.disconnect() currentDevice = null } // 停止投屏 if (isProjecting) { miracastManager.stopProjection() isProjecting = false } // 关闭线程池 executor.shutdown() try { if (!executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) { executor.shutdownNow() } } catch (e: InterruptedException) { executor.shutdownNow() } // 清理资源 mediaProjection?.stop() mediaProjection = null projectionData = null // 释放Handler资源 statusUpdateHandler = null statusUpdateRunnable = null Log.i(TAG, "ExpoDlnaPlayerModule资源已清理") } catch (e: Exception) { Log.e(TAG, "模块清理时出错", e) } } override fun definition() = ModuleDefinition { Name("ExpoDlnaPlayer") // 定义事件 Events( "onDeviceFound", "onDeviceDisappeared", "onConnectionChanged", "onPlaybackStatusChanged", "onError" ) // 设备发现API AsyncFunction("startDiscovery") { try { Log.i(TAG, "开始设备搜索...") // 启动DLNA设备发现 discoveryManagerInstance.startDiscovery { device, isNew -> if (isNew) { Log.i(TAG, "发现新DLNA设备: ${device.getString("name")} (${device.getString("id")})") // 先添加或更新设备 addOrUpdateDevice(device) Log.i(TAG, "当前设备列表大小: ${deviceList.size}") // 检查设备是否有controlURL val controlURL = device.optString("controlURL", "") Log.i(TAG, "设备控制URL: $controlURL") // 确保设备Map包含所有必要字段后发送事件 val deviceMap = device.toMap().toMutableMap() if (controlURL.isNotEmpty() && !deviceMap.containsKey("controlURL")) { deviceMap["controlURL"] = controlURL Log.i(TAG, "显式添加controlURL到设备事件: $controlURL") } sendEvent("onDeviceFound", deviceMap) } } // 如果设备支持,同时启动Miracast设备发现 if (miracastManager.isProjectionSupported()) { Log.i(TAG, "设备支持Miracast,开始搜索Miracast设备...") miracastManager.startDiscovery { device, isNew -> if (isNew) { Log.i(TAG, "发现新Miracast设备: ${device.getString("name")} (${device.getString("id")})") // Miracast设备已在MiracastManager中添加到deviceList addOrUpdateDevice(device) // 确保设备Map包含所有必要字段后发送事件 val deviceMap = device.toMap().toMutableMap() val controlURL = device.optString("controlURL", "") if (controlURL.isNotEmpty() && !deviceMap.containsKey("controlURL")) { deviceMap["controlURL"] = controlURL Log.i(TAG, "显式添加controlURL到Miracast设备事件: $controlURL") } sendEvent("onDeviceFound", deviceMap) } } } else { Log.i(TAG, "设备不支持Miracast,跳过Miracast设备搜索") } Log.i(TAG, "设备搜索已启动") } catch (e: Exception) { Log.e(TAG, "启动设备搜索失败", e) sendErrorEvent("DISCOVERY_START_ERROR", e.message ?: "Failed to start discovery") throw e } } AsyncFunction("stopDiscovery") { try { Log.i(TAG, "停止设备搜索...") deviceList.clear() // 清空设备列表,防止重复搜索时出现问题 // 停止DLNA设备发现 discoveryManagerInstance.stopDiscovery() // 停止Miracast设备发现 miracastManager.stopDiscovery() Log.i(TAG, "设备搜索已停止") } catch (e: Exception) { Log.e(TAG, "停止设备搜索失败", e) sendErrorEvent("DISCOVERY_STOP_ERROR", e.message ?: "Failed to stop discovery") throw e } } AsyncFunction("getDevices") { Log.i(TAG, "获取设备列表,当前设备数量: ${deviceList.size}") synchronized(deviceListLock) { deviceList.values.map { device -> val deviceId = device.optString("id") val isConnected = currentDevice?.optString("id") == deviceId val controlURL = device.optString("controlURL", "") // 记录每个设备的controlURL Log.i(TAG, "设备 ${device.optString("name")} ($deviceId) 的controlURL: $controlURL") // 将设备转换为Map val deviceMap = device.toMap().toMutableMap() // 显式添加设备状态和controlURL deviceMap["isConnected"] = isConnected // 确保controlURL被正确传递 if (controlURL.isNotEmpty()) { deviceMap["controlURL"] = controlURL Log.i(TAG, "显式添加controlURL到设备Map: $controlURL") } else { // 检查是否有备选URL val altControlURL = device.optString("activeServiceControl", "") if (altControlURL.isNotEmpty()) { deviceMap["controlURL"] = altControlURL Log.i(TAG, "使用备选activeServiceControl作为controlURL: $altControlURL") } else { Log.w(TAG, "设备无controlURL可用") } } // 检查serviceList是否被正确转换 if (deviceMap.containsKey("serviceList")) { val serviceList = deviceMap["serviceList"] as? Map<*, *> if (serviceList != null) { val avTransport = serviceList["AVTransport"] as? Map<*, *> if (avTransport != null) { val serviceControlURL = avTransport["controlURL"] as? String Log.i(TAG, "serviceList中的AVTransport controlURL: $serviceControlURL") // 如果设备Map没有controlURL但serviceList中有,则使用它 if (!deviceMap.containsKey("controlURL") || (deviceMap["controlURL"] as? String).isNullOrEmpty()) { if (!serviceControlURL.isNullOrEmpty()) { deviceMap["controlURL"] = serviceControlURL Log.i(TAG, "从serviceList中提取controlURL: $serviceControlURL") } } } } } // 最后一次检查,确保controlURL被包含 if (!deviceMap.containsKey("controlURL") || (deviceMap["controlURL"] as? String).isNullOrEmpty()) { Log.e(TAG, "警告:设备仍然没有controlURL") } else { Log.i(TAG, "最终传递给前端的controlURL: ${deviceMap["controlURL"]}") } deviceMap } } } // 连接与控制API AsyncFunction("connectToDevice") { deviceId: String -> try { Log.i(TAG, "尝试连接设备: $deviceId") // 检查是否已连接 if (currentDevice != null) { val currentId = currentDevice?.optString("id") if (currentId == deviceId) { Log.i(TAG, "设备已经连接: $deviceId") return@AsyncFunction true } else { Log.i(TAG, "断开当前设备连接: $currentId") connectionManagerInstance.disconnect() currentDevice = null } } // 获取设备信息 val device = getDevice(deviceId) ?: run { Log.e(TAG, "连接失败:找不到设备 $deviceId") throw Exception("找不到设备,请确保设备在线并重新搜索") } // 获取设备类型并根据类型连接 val deviceType = device.optString("type", "") Log.i(TAG, "设备类型: $deviceType, 设备名称: ${device.optString("name")}") var result = false when (deviceType) { "dlna" -> { // 验证必要参数,并尝试从renderControlURL推断controlURL var controlURL = device.optString("controlURL", "") // 如果没有controlURL但有renderControlURL,尝试推断 if (controlURL.isEmpty()) { val renderControlURL = device.optString("renderControlURL", "") if (renderControlURL.isNotEmpty()) { Log.i(TAG, "设备没有controlURL但有renderControlURL: $renderControlURL") // 尝试从renderControlURL构造controlURL controlURL = renderControlURL.replace("RenderingControl", "AVTransport") Log.i(TAG, "基于renderControlURL推断的controlURL: $controlURL") // 将推断的URL保存到设备对象中 device.put("controlURL", controlURL) Log.i(TAG, "已将推断的controlURL添加到设备对象") } } // 再次检查是否有controlURL if (controlURL.isEmpty()) { Log.e(TAG, "DLNA设备缺少控制URL,无法推断") throw Exception("设备不支持DLNA控制") } // 使用DLNA协议连接 result = connectionManagerInstance.connect(device) if (result) { currentDevice = device Log.i(TAG, "DLNA设备连接成功: ${device.optString("name")}") } } "miracast" -> { // 验证必要参数 if (!miracastManager.isProjectionSupported()) { Log.e(TAG, "当前设备不支持Miracast投屏") throw Exception("您的设备不支持Miracast投屏功能") } if (device.optString("routeName", "").isEmpty()) { Log.e(TAG, "Miracast设备缺少路由信息") throw Exception("设备不支持Miracast连接") } // 使用Miracast投屏 result = miracastManager.startProjection(device, SCREEN_MIRRORING) if (result) { isProjecting = true currentDevice = device Log.i(TAG, "Miracast设备连接成功: ${device.optString("name")}") } } "airplay" -> { // AirPlay设备处理 result = connectionManagerInstance.connect(device) if (result) { currentDevice = device Log.i(TAG, "AirPlay设备连接成功: ${device.optString("name")}") } } else -> { Log.e(TAG, "不支持的设备类型: $deviceType") throw Exception("不支持的设备类型: $deviceType") } } if (result) { // 发送连接状态变更事件 mainHandler.post { sendEvent("onConnectionChanged", mapOf( "deviceId" to deviceId, "connected" to true, "deviceName" to device.optString("name"), "deviceType" to device.optString("type") )) } } else { Log.e(TAG, "设备连接失败: ${device.optString("name")} ($deviceId)") throw Exception("连接失败,请重试") } result } catch (e: Exception) { val errorMessage = e.message ?: "连接设备失败" Log.e(TAG, "连接错误: $errorMessage", e) sendErrorEvent("CONNECTION_ERROR", errorMessage, deviceId) throw e } } AsyncFunction("disconnectFromDevice") { try { val device = currentDevice ?: return@AsyncFunction null val deviceId = device.getString("id") val deviceType = device.optString("type", "") Log.i(TAG, "断开设备连接: ${device.optString("name")} ($deviceId), 类型: $deviceType") // 停止状态更新任务 stopStatusUpdates() when (deviceType) { "miracast" -> { if (isProjecting) { miracastManager.stopProjection() isProjecting = false Log.i(TAG, "Miracast投屏已停止") } } else -> { // DLNA 或其他设备使用通用连接管理器断开 connectionManagerInstance.disconnect() Log.i(TAG, "DLNA/AirPlay设备已断开连接") } } currentDevice = null sendEvent("onConnectionChanged", mapOf( "deviceId" to deviceId, "connected" to false, "deviceType" to deviceType )) null } catch (e: Exception) { Log.e(TAG, "断开连接失败", e) sendErrorEvent("DISCONNECT_ERROR", e.message ?: "断开设备连接失败") throw e } } AsyncFunction("isConnected") { currentDevice != null && connectionManagerInstance.isConnected() } AsyncFunction("getConnectedDevice") { currentDevice?.toMap() } // 媒体控制API AsyncFunction("play") { url: String, title: String?, mimeType: String? -> try { if (currentDevice == null) { throw Exception("未连接设备") } val mediaTitle = title ?: "Media" val mediaMimeType = mimeType ?: guessMimeType(url) val deviceType = currentDevice?.optString("type", "") Log.i(TAG, "播放媒体: $url, 类型: $mediaMimeType, 设备类型: $deviceType") when (deviceType) { "miracast" -> { if (!isProjecting) { throw Exception("Miracast投屏未启动,请先启动投屏") } // 针对Miracast的视频播放 if (miracastManager.canPlayVideo()) { val result = miracastManager.playVideo(url, mediaTitle, mediaMimeType) if (!result) { throw Exception("Miracast视频播放失败") } Log.i(TAG, "Miracast视频播放已启动") } else { throw Exception("当前Miracast模式不支持视频播放,请使用VIDEO_ONLY模式") } } else -> { // DLNA/AirPlay 设备使用通用连接管理器播放 connectionManagerInstance.play(url, mediaTitle, mediaMimeType) Log.i(TAG, "DLNA/AirPlay媒体播放已启动") } } updatePlaybackStatus() } catch (e: Exception) { Log.e(TAG, "播放媒体失败", e) sendErrorEvent("PLAY_ERROR", e.message ?: "播放失败", currentDevice?.optString("id")) throw e } } AsyncFunction("pause") { try { if (currentDevice == null) { throw Exception("No device connected") } connectionManagerInstance.pause() updatePlaybackStatus() } catch (e: Exception) { sendErrorEvent("PAUSE_ERROR", e.message ?: "Failed to pause media") throw e } } AsyncFunction("resume") { try { if (currentDevice == null) { throw Exception("No device connected") } connectionManagerInstance.resume() updatePlaybackStatus() } catch (e: Exception) { sendErrorEvent("RESUME_ERROR", e.message ?: "Failed to resume media") throw e } } AsyncFunction("stop") { try { if (currentDevice == null) { throw Exception("No device connected") } connectionManagerInstance.stop() updatePlaybackStatus() } catch (e: Exception) { sendErrorEvent("STOP_ERROR", e.message ?: "Failed to stop media") throw e } } AsyncFunction("seek") { position: Double -> try { if (currentDevice == null) { throw Exception("No device connected") } connectionManagerInstance.seek(position.toLong()) updatePlaybackStatus() } catch (e: Exception) { sendErrorEvent("SEEK_ERROR", e.message ?: "Failed to seek media") throw e } } AsyncFunction("setVolume") { volume: Double -> try { if (currentDevice == null) { throw Exception("No device connected") } connectionManagerInstance.setVolume(volume.toInt()) updatePlaybackStatus() } catch (e: Exception) { sendErrorEvent("VOLUME_ERROR", e.message ?: "Failed to set volume") throw e } } AsyncFunction("getPlaybackStatus") { try { if (currentDevice == null) { throw Exception("No device connected") } connectionManagerInstance.getPlaybackStatus() } catch (e: Exception) { sendErrorEvent("STATUS_ERROR", e.message ?: "Failed to get playback status") throw e } } // 新增:设置播放速率功能 AsyncFunction("setRate") { rate: Double -> try { if (currentDevice == null) { throw Exception("设备未连接") } // 确保速率在合理范围内 val safeRate = rate.coerceIn(0.5, 2.0) Log.i(TAG, "设置播放速率: $safeRate") connectionManagerInstance.setRate(safeRate) updatePlaybackStatus() true } catch (e: Exception) { Log.e(TAG, "设置播放速率失败", e) sendErrorEvent("RATE_ERROR", e.message ?: "设置播放速率失败") throw e } } // 新增:设置静音功能 AsyncFunction("setMuted") { muted: Boolean -> try { if (currentDevice == null) { throw Exception("设备未连接") } Log.i(TAG, "设置静音状态: $muted") connectionManagerInstance.setMuted(muted) updatePlaybackStatus() true } catch (e: Exception) { Log.e(TAG, "设置静音状态失败", e) sendErrorEvent("MUTE_ERROR", e.message ?: "设置静音状态失败") throw e } } // 新增:获取缓冲状态 AsyncFunction("getBufferingStatus") { try { if (currentDevice == null) { throw Exception("设备未连接") } connectionManagerInstance.getBufferingStatus() } catch (e: Exception) { Log.e(TAG, "获取缓冲状态失败", e) sendErrorEvent("BUFFER_ERROR", e.message ?: "获取缓冲状态失败") throw e } } // Miracast相关API AsyncFunction("isProjectionSupported") { miracastManager.isProjectionSupported() } AsyncFunction("startProjection") { deviceId: String, mode: String? -> try { if (isProjecting) { sendEvent("onError", mapOf( "code" to "ALREADY_PROJECTING", "message" to "已有投屏正在进行中" )) return@AsyncFunction false } val device = getDevice(deviceId) if (device == null) { sendEvent("onError", mapOf( "code" to "DEVICE_NOT_FOUND", "message" to "找不到指定设备" )) return@AsyncFunction false } if (device.optString("type") != "miracast") { sendEvent("onError", mapOf( "code" to "INVALID_DEVICE_TYPE", "message" to "该设备不支持Miracast投屏" )) return@AsyncFunction false } val result = miracastManager.startProjection(device, mode ?: SCREEN_MIRRORING) if (result) { isProjecting = true currentDevice = device sendEvent("onConnectionChanged", mapOf( "deviceId" to deviceId, "connected" to true )) } result } catch (e: Exception) { Log.e(TAG, "启动投屏失败", e) sendEvent("onError", mapOf( "code" to "PROJECTION_ERROR", "message" to "启动投屏失败: ${e.message ?: "未知错误"}" )) false } } AsyncFunction("stopProjection") { try { if (!isProjecting) { return@AsyncFunction true } val device = currentDevice if (device != null && device.optString("type") == "miracast") { val deviceId = device.getString("id") miracastManager.stopProjection() isProjecting = false sendEvent("onConnectionChanged", mapOf( "deviceId" to deviceId, "connected" to false )) } else { Log.i(TAG, "没有正在进行的Miracast投屏") } true } catch (e: Exception) { Log.e(TAG, "停止投屏失败", e) sendEvent("onError", mapOf( "code" to "STOP_PROJECTION_ERROR", "message" to "停止投屏失败: ${e.message ?: "未知错误"}" )) false } } // 常量定义 Constants( "SCREEN_MIRRORING" to SCREEN_MIRRORING, "VIDEO_ONLY" to VIDEO_ONLY ) // 添加资源释放API AsyncFunction("release") { cleanUp() true } } private fun updatePlaybackStatus() { executor.execute { try { val status = connectionManagerInstance.getPlaybackStatus() mainHandler.post { sendEvent("onPlaybackStatusChanged", status) } } catch (e: Exception) { Log.e(TAG, "Error updating playback status", e) } } } // 新增:定期更新播放状态的计划任务 private var statusUpdateHandler: Handler? = null private var statusUpdateRunnable: Runnable? = null private val STATUS_UPDATE_INTERVAL = 2000L // 2秒更新一次 // 开始定期更新播放状态 private fun startStatusUpdates() { if (statusUpdateHandler == null) { statusUpdateHandler = Handler(Looper.getMainLooper()) } // 停止之前的任务 stopStatusUpdates() // 创建新的定期任务 statusUpdateRunnable = object : Runnable { override fun run() { if (currentDevice != null && connectionManagerInstance.isConnected()) { updatePlaybackStatus() statusUpdateHandler?.postDelayed(this, STATUS_UPDATE_INTERVAL) } else { // 如果设备已断开,停止更新 stopStatusUpdates() } } } // 启动定期任务 statusUpdateHandler?.post(statusUpdateRunnable!!) Log.i(TAG, "已启动播放状态定期更新") } // 停止定期更新播放状态 private fun stopStatusUpdates() { statusUpdateRunnable?.let { statusUpdateHandler?.removeCallbacks(it) statusUpdateRunnable = null Log.i(TAG, "已停止播放状态定期更新") } } private fun sendErrorEvent(code: String, message: String, deviceId: String? = null) { val params = mutableMapOf( "code" to code, "message" to message ) if (deviceId != null) { params["deviceId"] = deviceId } // 记录错误日志 Log.e(TAG, "Error: [$code] $message${deviceId?.let { " (Device: $it)" } ?: ""}") // 发送事件 sendEvent("onError", params) } // 统一处理异常的辅助方法 private fun handleException(e: Exception, deviceId: String? = null): Exception { val message = e.message ?: "Unknown error during play media" Log.e(TAG, "play media failed: $message", e) sendErrorEvent("PLAY_ERROR", message, deviceId) return e } private fun guessMimeType(url: String): String { return when { url.endsWith(".mp4", true) -> "video/mp4" url.endsWith(".m3u8", true) -> "application/x-mpegURL" url.endsWith(".mpd", true) -> "application/dash+xml" url.endsWith(".mp3", true) -> "audio/mpeg" url.endsWith(".wav", true) -> "audio/wav" url.endsWith(".jpg", true) || url.endsWith(".jpeg", true) -> "image/jpeg" url.endsWith(".png", true) -> "image/png" else -> "video/mp4" // 默认视频类型 } } // DLNA设备发现管理器 private inner class DiscoveryManager(private val context: AppContext) { private val SSDP_HOST = "239.255.255.250" private val SSDP_PORT = 1900 private val SEARCH_TYPES = listOf( "ssdp:all", "urn:schemas-upnp-org:device:MediaRenderer:1", "urn:schemas-upnp-org:service:AVTransport:1", "urn:schemas-upnp-org:service:RenderingControl:1", "urn:schemas-upnp-org:device:MediaServer:1" ) private var isRunning = false private var multicastLock: WifiManager.MulticastLock? = null // 移除设备缓存,由前端负责设备管理 // private val deviceCache = mutableMapOf() private var discoveryWatchdog: Handler? = null private var discoveryCheckRunnable: Runnable? = null private val DISCOVERY_CHECK_INTERVAL = 30000L // 30秒检查一次 private val MAX_RETRY_COUNT = 3 // 最大重试次数 private var retryCount = 0 // 当前重试次数 // 添加一个原子布尔变量来控制接收线程 private var threadShouldRun = java.util.concurrent.atomic.AtomicBoolean(true) // 添加网络连接状态监听 private var networkCallback: ConnectivityManager.NetworkCallback? = null fun startDiscovery(onDeviceFound: (device: JSONObject, isNew: Boolean) -> Unit) { if (isRunning) { Log.i(TAG, "设备搜索已在运行中") return } // 重置重试计数 retryCount = 0 // 检查网络环境 if (!checkNetworkEnvironment()) { Log.e(TAG, "网络环境检查失败") return } isRunning = true Log.i(TAG, "初始化设备搜索...") // 获取WifiManager实例并请求MulticastLock val wifiManager = context.reactContext?.getSystemService(Context.WIFI_SERVICE) as? WifiManager if (wifiManager == null) { Log.e(TAG, "获取WifiManager失败") return } // 请求MulticastLock multicastLock = wifiManager.createMulticastLock("DLNAMulticastLock") multicastLock?.setReferenceCounted(true) multicastLock?.acquire() // 设置发现监测 setupDiscoveryWatchdog() // 设置网络状态监听 setupNetworkCallback() // 在后台线程运行SSDP发现 executor.execute { try { Log.i(TAG, "开始执行SSDP发现...") performSSDPDiscovery(onDeviceFound) } catch (e: Exception) { Log.e(TAG, "SSDP发现过程出错", e) handleDiscoveryError(e, onDeviceFound) } finally { if (isRunning) { // 如果仍应该运行,重新启动搜索 Log.i(TAG, "SSDP搜索周期完成,准备下一轮搜索...") executor.execute { try { performSSDPDiscovery(onDeviceFound) } catch (e: Exception) { Log.e(TAG, "重新启动SSDP发现过程出错", e) handleDiscoveryError(e, onDeviceFound) } } } else { stopDiscovery() } } } } // 处理发现过程中的错误 private fun handleDiscoveryError(error: Exception, onDeviceFound: (device: JSONObject, isNew: Boolean) -> Unit) { if (!isRunning) return when { error is java.net.SocketException && (error.message?.contains("Connection abort") == true || error.message?.contains("connection abort") == true) -> { if (retryCount < MAX_RETRY_COUNT) { retryCount++ Log.w(TAG, "检测到网络连接中断,尝试重新连接 (尝试 $retryCount/$MAX_RETRY_COUNT)") // 等待短暂时间后重试 Thread.sleep(3000) executor.execute { try { performSSDPDiscovery(onDeviceFound) } catch (e: Exception) { Log.e(TAG, "重试SSDP发现失败", e) handleDiscoveryError(e, onDeviceFound) } } } else { Log.e(TAG, "达到最大重试次数,停止重试") // 通知应用程序发现过程出错 mainHandler.post { sendEvent("onError", mapOf( "code" to "DISCOVERY_NETWORK_ERROR", "message" to "网络连接问题导致设备搜索失败" )) } } } else -> { Log.e(TAG, "未处理的发现错误: ${error.message}") // 通知应用程序发现过程出错 mainHandler.post { sendEvent("onError", mapOf( "code" to "DISCOVERY_ERROR", "message" to "设备搜索过程中发生错误: ${error.message ?: "未知错误"}" )) } } } } // 设置网络状态监听 private fun setupNetworkCallback() { try { val connectivityManager = context.reactContext?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager if (connectivityManager != null) { // 移除旧的回调(如果有) networkCallback?.let { try { connectivityManager.unregisterNetworkCallback(it) } catch (e: Exception) { Log.e(TAG, "移除旧的网络回调失败", e) } } // 创建新的网络回调 networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: android.net.Network) { Log.i(TAG, "网络连接恢复") retryCount = 0 // 重置重试计数 } override fun onLost(network: android.net.Network) { Log.w(TAG, "网络连接已断开") } } // 注册网络回调 val request = android.net.NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .build() connectivityManager.registerNetworkCallback(request, networkCallback!!) Log.i(TAG, "已注册网络状态监听器") } } catch (e: Exception) { Log.e(TAG, "设置网络状态监听器失败", e) } } private fun setupDiscoveryWatchdog() { // 初始化检查线程 discoveryWatchdog = Handler(Looper.getMainLooper()) discoveryCheckRunnable = Runnable { if (isRunning) { // 检查多播锁状态 if (multicastLock == null || !multicastLock!!.isHeld) { Log.w(TAG, "发现多播锁已丢失,重新获取...") try { val wifiManager = context.reactContext?.getSystemService(Context.WIFI_SERVICE) as? WifiManager if (wifiManager != null) { multicastLock = wifiManager.createMulticastLock("DLNAMulticastLock") multicastLock?.setReferenceCounted(true) multicastLock?.acquire() Log.i(TAG, "已重新获取多播锁") } } catch (e: Exception) { Log.e(TAG, "重新获取多播锁失败", e) } } // 重新调度检查 discoveryWatchdog?.postDelayed(discoveryCheckRunnable!!, DISCOVERY_CHECK_INTERVAL) } } // 启动周期性检查 discoveryWatchdog?.postDelayed(discoveryCheckRunnable!!, DISCOVERY_CHECK_INTERVAL) } fun stopDiscovery() { Log.i(TAG, "正在停止设备搜索...") isRunning = false // 停止检查线程 discoveryCheckRunnable?.let { discoveryWatchdog?.removeCallbacks(it) } discoveryWatchdog = null discoveryCheckRunnable = null // 取消注册网络回调 try { if (networkCallback != null) { val connectivityManager = context.reactContext?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager connectivityManager?.unregisterNetworkCallback(networkCallback!!) networkCallback = null Log.i(TAG, "已取消注册网络状态监听器") } } catch (e: Exception) { Log.e(TAG, "取消注册网络状态监听器失败", e) } try { multicastLock?.let { if (it.isHeld) { it.release() Log.i(TAG, "已释放多播锁") } } } catch (e: Exception) { Log.e(TAG, "释放多播锁失败", e) } multicastLock = null Log.i(TAG, "设备搜索已完全停止") } private fun getSearchMessage(searchType: String): String { return "M-SEARCH * HTTP/1.1\r\n" + "HOST: $SSDP_HOST:$SSDP_PORT\r\n" + "MAN: \"ssdp:discover\"\r\n" + "ST: $searchType\r\n" + "MX: 3\r\n" + "\r\n" } // 修改SSDP发现方法,始终通知前端设备发现 private fun performSSDPDiscovery(onDeviceFound: (device: JSONObject, isNew: Boolean) -> Unit) { var multicastSocket: java.net.MulticastSocket? = null var receiveThread = Thread() var group: InetAddress? = null try { Log.i(TAG, "创建SSDP套接字...") // 创建单一的MulticastSocket用于接收和发送 multicastSocket = java.net.MulticastSocket(null) multicastSocket.reuseAddress = true multicastSocket.bind(InetSocketAddress(InetAddress.getByName("0.0.0.0"), SSDP_PORT)) multicastSocket.soTimeout = 5000 multicastSocket.timeToLive = 32 // 设置TTL值 // 加入组播组 group = InetAddress.getByName(SSDP_HOST) multicastSocket.joinGroup(group) Log.i(TAG, "已加入组播组: $SSDP_HOST:$SSDP_PORT") // 记录自己的IP地址,用于过滤自己发送的消息 val localIpAddresses = getLocalIpAddresses() Log.i(TAG, "本机IP地址: $localIpAddresses") // 重置线程控制标志 threadShouldRun.set(true) // 创建接收线程 val receiveThreadLocal = Thread { val buffer = ByteArray(8192) val receivePacket = java.net.DatagramPacket(buffer, buffer.size) try { while (isRunning && threadShouldRun.get()) { try { Log.i(TAG, "等待SSDP响应...") // 在接收之前检查Socket是否已关闭 if (!multicastSocket.isClosed) { multicastSocket.receive(receivePacket) // 检查消息是否来自本机 val sourceAddress = receivePacket.address.hostAddress if (sourceAddress != null && localIpAddresses.contains(sourceAddress)) { Log.i(TAG, "收到来自本机的消息,跳过处理") continue } val response = String(receivePacket.data, 0, receivePacket.length) Log.i(TAG, "收到SSDP响应: ${response.substring(0, minOf(200, response.length))}...") // 解析响应 val deviceInfo = receivePacket.address.hostAddress?.let { parseSSDPResponse(response, it) } if (deviceInfo != null) { val deviceId = deviceInfo.getString("id") // 移除设备缓存检查,始终通知前端 Log.i(TAG, "发现设备: ${deviceInfo.getString("name")} (${deviceId})") // 无论设备是否为新设备,都通知前端 // 前端负责去重和管理设备列表 mainHandler.post { // 始终将isNew参数设置为true,让前端决定如何处理 onDeviceFound(deviceInfo, true) } } } else { // Socket已关闭,停止线程 Log.i(TAG, "接收Socket已关闭,停止接收线程") break } } catch (e: java.net.SocketTimeoutException) { // 接收超时,继续等待 if (isRunning && threadShouldRun.get()) { Log.i(TAG, "接收超时,继续等待...") } } catch (e: java.net.SocketException) { // Socket异常,可能是Socket已经关闭 if (e.message?.contains("closed") == true) { Log.i(TAG, "Socket已关闭,退出接收线程") break } else if (e.message?.contains("Connection abort") == true || e.message?.contains("connection abort") == true) { Log.e(TAG, "接收过程中连接中断", e) // 连接中断,跳出循环,将由错误处理重启 break } else if (isRunning && threadShouldRun.get()) { Log.e(TAG, "接收SSDP响应出错", e) } } catch (e: Exception) { if (isRunning && threadShouldRun.get()) { Log.e(TAG, "接收SSDP响应出错", e) } } } } catch (e: Exception) { Log.e(TAG, "接收线程异常", e) } finally { Log.i(TAG, "接收线程结束") } } receiveThreadLocal.start() receiveThread = receiveThreadLocal // 发送搜索请求循环 - 每5秒发送一次,重复多次 for (iteration in 1..3) { // 重复发送3轮 if (!isRunning) break for (searchType in SEARCH_TYPES) { if (!isRunning) break val searchMessage = getSearchMessage(searchType) val packet = searchMessage.toByteArray() val sendPacket = java.net.DatagramPacket(packet, packet.size, group, SSDP_PORT) try { Log.i(TAG, "发送SSDP搜索请求 #$iteration,类型: $searchType") multicastSocket.send(sendPacket) } catch (e: java.net.SocketException) { Log.e(TAG, "发送搜索请求时网络错误", e) throw e } // 每种搜索类型间隔1秒 Thread.sleep(1000) } // 每轮搜索后等待一段时间 Log.i(TAG, "完成搜索轮次 #$iteration,等待5秒...") Thread.sleep(5000) } // 总等待一段时间让设备响应 Thread.sleep(5000) } catch (e: Exception) { Log.e(TAG, "SSDP发现过程出错", e) throw e } finally { // 清理资源 - 首先通知线程停止 threadShouldRun.set(false) // 等待接收线程结束 if (isRunning) { try { Thread.sleep(2000) } catch (e: InterruptedException) { // 忽略中断异常 } } // 尝试中断线程(如果仍在运行) try { if (receiveThread.isAlive) { receiveThread.interrupt() Log.i(TAG, "已中断接收线程") } } catch (e: Exception) { Log.e(TAG, "中断接收线程时出错", e) } // 安全关闭Socket try { if (multicastSocket != null) { synchronized(multicastSocket) { if (!multicastSocket.isClosed) { try { // 检查group不为null再使用 if (group != null) { multicastSocket.leaveGroup(group) } } catch (e: Exception) { Log.e(TAG, "离开组播组时出错", e) } multicastSocket.close() Log.i(TAG, "已关闭组播Socket") } } } } catch (e: Exception) { Log.e(TAG, "关闭组播Socket出错", e) } } } // 修改解析SSDP响应的方法,不再检查设备是否存在于缓存中 private fun parseSSDPResponse(response: String, hostAddress: String): JSONObject? { try { // 尝试从回复中提取有用信息 val deviceInfo = JSONObject() // 提取重要字段 val locationMatch = Regex("(?i)LOCATION:\\s*(http://.*?)\\r?\\n").find(response) val location = locationMatch?.groupValues?.get(1) if (location == null || location.isEmpty()) { Log.w(TAG, "SSDP响应中没有有效的LOCATION字段") return null } // 确定消息类型并提取相应字段 if (response.contains("M-SEARCH * HTTP/1.1") || response.contains("HTTP/1.1 200 OK")) { // 处理M-SEARCH响应 val ntMatch = Regex("(?i)NT:\\s*(.*?)\\r?\\n").find(response) val stMatch = Regex("(?i)ST:\\s*(.*?)\\r?\\n").find(response) val usnMatch = Regex("(?i)USN:\\s*(.*?)\\r?\\n").find(response) val nt = ntMatch?.groupValues?.get(1) ?: "" val st = stMatch?.groupValues?.get(1) ?: "" val usn = usnMatch?.groupValues?.get(1) ?: "" Log.i(TAG, "SSDP设备: ST=${st}, NT=${nt}, 位置: $location") deviceInfo.put("LOCATION", location) if (nt.isNotEmpty()) deviceInfo.put("NT", nt) if (st.isNotEmpty()) deviceInfo.put("ST", st) if (usn.isNotEmpty()) deviceInfo.put("USN", usn) } else if (response.contains("NOTIFY * HTTP/1.1")) { // 处理NOTIFY消息 val ntMatch = Regex("(?i)NT:\\s*(.*?)\\r?\\n").find(response) val usnMatch = Regex("(?i)USN:\\s*(.*?)\\r?\\n").find(response) val nt = ntMatch?.groupValues?.get(1) ?: "" val usn = usnMatch?.groupValues?.get(1) ?: "" Log.i(TAG, "SSDP通知: NT=${nt}, 位置: $location") deviceInfo.put("LOCATION", location) if (nt.isNotEmpty()) deviceInfo.put("NT", nt) if (usn.isNotEmpty()) deviceInfo.put("USN", usn) } else { Log.w(TAG, "未知的SSDP消息类型") return null } // 获取设备的详细信息 return processDeviceInfo( location, deviceInfo.optString("NT", deviceInfo.optString("ST", "")), deviceInfo.optString("USN"), hostAddress ) } catch (e: Exception) { Log.e(TAG, "解析SSDP响应失败", e) return null } } // 处理设备信息的辅助方法 private fun processDeviceInfo(location: String, type: String?, usn: String?, hostAddress: String): JSONObject? { try { // 放宽过滤条件,接受更多潜在的媒体设备 val isMediaDevice = (type?.contains("MediaRenderer") == true || type?.contains("MediaServer") == true || usn?.contains("MediaRenderer") == true || usn?.contains("MediaServer") == true || type?.contains("AVTransport") == true || type?.contains("RenderingControl") == true || usn?.contains("AVTransport") == true) if (!isMediaDevice) { // 记录非媒体设备但仍尝试获取设备信息 Log.i(TAG, "响应可能不是媒体设备,但仍继续处理: type=$type, USN=$usn") } // 获取设备详细信息 Log.i(TAG, "获取设备详情: $location") val deviceDetails = fetchDeviceDescription(location) if (deviceDetails != null) { Log.i(TAG, "成功获取设备详情: ${deviceDetails.optString("friendlyName")}") // 提取UDN,确保它是有效的 var deviceId = deviceDetails.optString("UDN", "") if (deviceId.isEmpty()) { deviceId = UUID.randomUUID().toString() Log.i(TAG, "设备没有UDN,生成随机ID: $deviceId") } else if (deviceId.startsWith("uuid:")) { deviceId = deviceId.substring(5) Log.i(TAG, "从UDN中提取UUID: $deviceId") } return JSONObject().apply { put("id", deviceId) put("name", deviceDetails.optString("friendlyName", "未知设备")) put("model", deviceDetails.optString("modelName", "")) put("manufacturer", deviceDetails.optString("manufacturer", "")) put("ipAddress", hostAddress) put("location", location) put("type", "dlna") // 保存控制和事件订阅URL val serviceList = deviceDetails.optJSONObject("serviceList") if (serviceList != null) { val avTransport = findService(serviceList, "AVTransport") val renderingControl = findService(serviceList, "RenderingControl") if (avTransport != null) { val controlURL = avTransport.optString("controlURL", "") Log.i(TAG, "发现AVTransport服务,控制URL: $controlURL") if (controlURL.isEmpty()) { Log.w(TAG, "警告:AVTransport的控制URL为空") } put("controlURL", controlURL) put("eventSubURL", avTransport.optString("eventSubURL", "")) } else { Log.i(TAG, "设备没有AVTransport服务") } if (renderingControl != null) { val renderControlURL = renderingControl.optString("controlURL", "") Log.i(TAG, "发现RenderingControl服务,控制URL: $renderControlURL") put("renderControlURL", renderControlURL) } else { Log.i(TAG, "设备没有RenderingControl服务") } } else { Log.i(TAG, "设备没有服务列表") } } } else { Log.e(TAG, "获取设备详情失败") } } catch (e: Exception) { Log.e(TAG, "处理设备信息出错", e) } return null } // 修改为使用DOM解析器解析设备描述文档 private fun fetchDeviceDescription(location: String): JSONObject? { var connection: HttpURLConnection? = null var inputStream: java.io.InputStream? = null try { Log.i(TAG, "开始获取设备描述: $location") val url = URL(location) connection = url.openConnection() as HttpURLConnection connection.requestMethod = "GET" connection.connectTimeout = 10000 connection.readTimeout = 10000 val responseCode = connection.responseCode Log.i(TAG, "设备描述HTTP响应码: $responseCode") if (responseCode == HttpURLConnection.HTTP_OK) { inputStream = connection.inputStream // 将XML响应读入字符串以进行日志记录 val responseString = inputStream.bufferedReader().use { it.readText() } Log.d(TAG, "设备描述XML: $responseString") // 重新创建输入流进行解析 inputStream = responseString.byteInputStream() // 使用DOM解析XML val factory = javax.xml.parsers.DocumentBuilderFactory.newInstance() factory.isNamespaceAware = true try { // 禁用外部实体解析,防止XXE攻击 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) factory.setFeature("http://xml.org/sax/features/external-general-entities", false) factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false) } catch (e: Exception) { Log.w(TAG, "无法设置XML安全特性", e) } val builder = factory.newDocumentBuilder() val document = builder.parse(inputStream) document.documentElement.normalize() // 创建结果对象 val result = JSONObject() // 从location获取默认URLBase var urlBase = "${url.protocol}://${url.host}:${url.port}" result.put("baseUrl", urlBase) // 首先尝试获取URLBase元素(在root级别) val urlBaseList = document.getElementsByTagName("URLBase") if (urlBaseList.length > 0) { val urlBaseText = urlBaseList.item(0).textContent?.trim() if (!urlBaseText.isNullOrEmpty()) { urlBase = urlBaseText Log.i(TAG, "从XML获取URLBase: $urlBase") result.put("xmlUrlBase", urlBase) } } else { Log.i(TAG, "使用默认URLBase: $urlBase") } // 获取device元素(不使用命名空间,更通用) val deviceNodes = document.getElementsByTagName("device") if (deviceNodes.length == 0) { Log.e(TAG, "XML中未找到device元素") return null } val deviceElement = deviceNodes.item(0) as org.w3c.dom.Element // 提取设备基本信息 fun getElementText(parent: org.w3c.dom.Element, tagName: String): String { val nodeList = parent.getElementsByTagName(tagName) return if (nodeList.length > 0) nodeList.item(0).textContent ?: "" else "" } val friendlyName = getElementText(deviceElement, "friendlyName") Log.i(TAG, "设备名称: $friendlyName") result.put("friendlyName", friendlyName) val manufacturer = getElementText(deviceElement, "manufacturer") Log.i(TAG, "制造商: $manufacturer") result.put("manufacturer", manufacturer) val modelName = getElementText(deviceElement, "modelName") Log.i(TAG, "型号: $modelName") result.put("modelName", modelName) val udn = getElementText(deviceElement, "UDN") Log.i(TAG, "UDN: $udn") result.put("UDN", udn) // 处理服务列表 val serviceListElements = deviceElement.getElementsByTagName("serviceList") if (serviceListElements.length > 0) { Log.i(TAG, "找到服务列表,开始解析服务") val serviceList = JSONObject() val serviceListElement = serviceListElements.item(0) as org.w3c.dom.Element val serviceElements = serviceListElement.getElementsByTagName("service") Log.i(TAG, "发现服务数量: ${serviceElements.length}") // 直接变量,用于保存AVTransport的控制URL var avTransportControlURL = "" var renderingControlURL = "" for (i in 0 until serviceElements.length) { val serviceElement = serviceElements.item(i) as org.w3c.dom.Element val serviceType = getElementText(serviceElement, "serviceType") val serviceId = getElementText(serviceElement, "serviceId") val controlURL = getElementText(serviceElement, "controlURL") val eventSubURL = getElementText(serviceElement, "eventSubURL") val scpdURL = getElementText(serviceElement, "SCPDURL") Log.i(TAG, "原始服务数据: type=$serviceType, id=$serviceId") Log.i(TAG, "原始controlURL: '$controlURL'") // 解析完整URL val fullControlURL = resolveUrlWithBase(urlBase, controlURL) val fullEventSubURL = resolveUrlWithBase(urlBase, eventSubURL) val fullSCPDURL = resolveUrlWithBase(urlBase, scpdURL) Log.i(TAG, "解析后controlURL: '$fullControlURL'") val service = JSONObject() service.put("serviceType", serviceType) service.put("serviceId", serviceId) service.put("controlURL", fullControlURL) service.put("eventSubURL", fullEventSubURL) service.put("SCPDURL", fullSCPDURL) // 直接在设备根级别保存重要的URL if (serviceType.contains("AVTransport")) { Log.i(TAG, "找到AVTransport服务! 控制URL: $fullControlURL") avTransportControlURL = fullControlURL result.put("activeServiceControl", fullControlURL) result.put("controlURL", fullControlURL) // 明确在根级别添加 result.put("avTransportControlURL", fullControlURL) // 添加额外的备用名称 } if (serviceType.contains("RenderingControl")) { Log.i(TAG, "找到RenderingControl服务! 控制URL: $fullControlURL") renderingControlURL = fullControlURL result.put("renderControlURL", fullControlURL) } // 使用服务类型的最后部分作为键 val serviceName = serviceType.substringAfterLast(":") serviceList.put(serviceName, service) } // 确保直接在结果根对象中保存AVTransport控制URL if (avTransportControlURL.isNotEmpty()) { // 这是第二次设置,再次确保它被保存 result.put("controlURL", avTransportControlURL) Log.i(TAG, "在结果根对象中保存controlURL: $avTransportControlURL") } else { Log.w(TAG, "未找到AVTransport服务,无法提取controlURL") } result.put("serviceList", serviceList) } else { Log.w(TAG, "未找到serviceList元素") } return result } else { Log.e(TAG, "获取设备描述失败,HTTP响应码: $responseCode") } } catch (e: Exception) { Log.e(TAG, "获取设备描述失败", e) } finally { try { inputStream?.close() connection?.disconnect() } catch (e: Exception) { Log.e(TAG, "关闭资源失败", e) } } return null } // 使用urlBase正确解析相对路径 private fun resolveUrlWithBase(urlBase: String, path: String): String { if (path.isEmpty()) return "" if (path.startsWith("http")) return path // 强制打印值以便调试 Log.d(TAG, "开始组合URL: base='$urlBase', path='$path'") // 简单直接的URL组合逻辑 val result = when { // 如果path已经以/开头,直接拼接 path.startsWith("/") -> { if (urlBase.endsWith("/")) urlBase.substring(0, urlBase.length - 1) + path else urlBase + path } // 如果path不以/开头,确保中间有/ else -> { if (urlBase.endsWith("/")) urlBase + path else "$urlBase/$path" } } Log.d(TAG, "URL组合结果: '$result'") return result } private fun findService(serviceList: JSONObject, serviceName: String): JSONObject? { try { // 首先尝试直接获取服务 val service = serviceList.optJSONObject(serviceName) if (service != null) { Log.i(TAG, "直接通过键名找到服务: $serviceName") return service } // 如果直接获取失败,遍历所有服务查找匹配的服务类型 Log.i(TAG, "尝试遍历查找服务: $serviceName") val keys = serviceList.keys() while (keys.hasNext()) { val key = keys.next() val serviceObj = serviceList.optJSONObject(key) if (serviceObj != null) { // 检查serviceType是否包含所需服务名称 val serviceType = serviceObj.optString("serviceType", "") if (serviceType.contains(serviceName, ignoreCase = true)) { Log.i(TAG, "通过服务类型找到服务: $key (类型: $serviceType)") return serviceObj } } } Log.w(TAG, "未找到服务: $serviceName") return null } catch (e: Exception) { Log.e(TAG, "查找服务时出错: $serviceName", e) return null } } private fun checkNetworkEnvironment(): Boolean { val wifiManager = context.reactContext?.getSystemService(Context.WIFI_SERVICE) as? WifiManager if (wifiManager == null) { Log.e(TAG, "获取WifiManager失败") return false } // 检查WiFi是否开启 if (!wifiManager.isWifiEnabled) { Log.e(TAG, "WiFi未开启") return false } // 检查是否连接到网络 val connectivityManager = context.reactContext?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager if (connectivityManager == null) { Log.e(TAG, "获取ConnectivityManager失败") return false } // 使用新的API检查WiFi连接 val network = connectivityManager.activeNetwork if (network == null) { Log.e(TAG, "未连接到任何网络") return false } val capabilities = connectivityManager.getNetworkCapabilities(network) if (capabilities == null || !capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { Log.e(TAG, "未连接到WiFi网络") return false } // 检查多播是否可用 try { val multicastSocket = java.net.MulticastSocket() multicastSocket.close() return true } catch (e: Exception) { Log.e(TAG, "多播不可用", e) return false } } // 获取本机所有网络接口的IP地址 private fun getLocalIpAddresses(): List { val addresses = mutableListOf() try { val interfaces = java.net.NetworkInterface.getNetworkInterfaces() while (interfaces.hasMoreElements()) { val networkInterface = interfaces.nextElement() val inetAddresses = networkInterface.inetAddresses while (inetAddresses.hasMoreElements()) { val address = inetAddresses.nextElement() if (!address.isLoopbackAddress && address is java.net.Inet4Address) { address.hostAddress?.let { addresses.add(it) } } } } } catch (e: Exception) { Log.e(TAG, "获取本机IP地址失败", e) } return addresses } } // DLNA连接和控制管理器 private inner class ConnectionManager { private var currentDevice: JSONObject? = null private var isConnected: Boolean = false private var mediaUrl: String? = null private var mediaTitle: String? = null private var mediaMimeType: String? = null private var transportState: String = "STOPPED" private var duration: Long = 0 private var position: Long = 0 private var volume: Int = 50 // 新增播放速率和静音状态属性 private var rate: Double = 1.0 private var isMuted: Boolean = false private var isBuffering: Boolean = false fun connect(device: JSONObject): Boolean { try { Log.i(TAG, "ConnectionManager: 开始连接设备 ${device.optString("name")}") // 检查设备类型 when (device.optString("type")) { "dlna" -> { // 收集所有可能的控制URL val possibleControlURLs = mutableListOf() // 主URL val controlURL = device.optString("controlURL") if (controlURL.isNotEmpty()) { possibleControlURLs.add(controlURL) Log.i(TAG, "尝试主controlURL: $controlURL") } // 备选URLs for (i in 1..5) { val altURL = device.optString("alternateServiceControl$i") if (altURL.isNotEmpty()) { possibleControlURLs.add(altURL) Log.i(TAG, "备选controlURL $i: $altURL") } } // 如果没有找到任何URL,检查是否有serviceList if (possibleControlURLs.isEmpty()) { val serviceList = device.optJSONObject("serviceList") if (serviceList != null) { // 查找AVTransport服务 val avTransport = serviceList.optJSONObject("AVTransport") if (avTransport != null) { val avControlURL = avTransport.optString("controlURL") if (avControlURL.isNotEmpty()) { possibleControlURLs.add(avControlURL) Log.i(TAG, "从serviceList中获取到controlURL: $avControlURL") } } } } // 尝试从renderControlURL推断 if (possibleControlURLs.isEmpty()) { val renderControlURL = device.optString("renderControlURL", "") if (renderControlURL.isNotEmpty()) { val predictedURL = renderControlURL.replace("RenderingControl", "AVTransport") possibleControlURLs.add(predictedURL) Log.i(TAG, "从renderControlURL推断的controlURL: $predictedURL") } } if (possibleControlURLs.isEmpty()) { Log.e(TAG, "ConnectionManager: DLNA设备缺少控制URL") return false } // 尝试每个可能的URL var connectionSuccess = false var workingControlURL = "" for (testURL in possibleControlURLs) { Log.i(TAG, "正在测试控制URL: $testURL") // 尝试发送测试命令验证连接 val maxRetry = 1 // 每个URL的最大重试次数 var retryCount = 0 while (retryCount <= maxRetry) { try { // 准备请求参数 val arguments = mapOf("InstanceID" to "0") sendSoapRequest(testURL, "urn:schemas-upnp-org:service:AVTransport:1", "GetTransportInfo", arguments) Log.i(TAG, "ConnectionManager: URL有效! $testURL") // 如果代码执行到这里,说明连接成功 connectionSuccess = true workingControlURL = testURL break } catch (e: Exception) { Log.w(TAG, "ConnectionManager: 测试URL失败 $testURL", e) retryCount++ if (retryCount <= maxRetry) { Log.w(TAG, "连接失败,尝试重试 ($retryCount/$maxRetry)") Thread.sleep(500) } } } if (connectionSuccess) { break // 找到有效URL,停止测试 } } if (!connectionSuccess) { Log.e(TAG, "ConnectionManager: 所有控制URL都无效") return false } // 更新设备的控制URL为可用的URL device.put("controlURL", workingControlURL) Log.i(TAG, "ConnectionManager: 已更新为可用的控制URL: $workingControlURL") // 设置当前设备 currentDevice = device isConnected = true return true } "miracast" -> { // Miracast设备的连接逻辑已在MiracastManager中处理 Log.i(TAG, "ConnectionManager: Miracast设备将通过系统对话框连接") return true } "airplay" -> { // AirPlay设备处理 Log.i(TAG, "ConnectionManager: 连接AirPlay设备") currentDevice = device isConnected = true return true } else -> { Log.e(TAG, "ConnectionManager: 不支持的设备类型 ${device.optString("type")}") return false } } } catch (e: Exception) { Log.e(TAG, "ConnectionManager: 连接失败", e) return false } } fun disconnect() { try { // 停止播放 stop() // 停止状态更新 mainHandler.post { stopStatusUpdates() } // 重置状态 currentDevice = null isConnected = false mediaUrl = null mediaTitle = null mediaMimeType = null transportState = "STOPPED" duration = 0 position = 0 rate = 1.0 isMuted = false isBuffering = false } catch (e: Exception) { Log.e(TAG, "Disconnection error", e) } } fun isConnected(): Boolean { return isConnected } /** * 构建DIDL-Lite元数据 * @param url 媒体URL * @param title 媒体标题 * @param mimeType 媒体MIME类型 * @return DIDL-Lite元数据 */ private fun buildDIDLMetadata(url: String, title: String, mimeType: String): String { return """ $title object.item.videoItem $url """.trimIndent() } fun play(url: String, title: String, mimeType: String) { try { mediaUrl = url mediaTitle = title mediaMimeType = mimeType val device = currentDevice ?: throw Exception("No device connected") val controlURL = device.optString("controlURL") if (controlURL.isBlank()) { throw Exception("Device has no control URL") } Log.i(TAG, "准备播放媒体: $url") Log.i(TAG, "媒体类型: $mimeType, 标题: $title") // 设置初始状态 isBuffering = true // 通知前端开始缓冲 mainHandler.post { sendEvent("onPlaybackStatusChanged", mapOf( "isPlaying" to false, "isBuffering" to true, "duration" to 0, "position" to 0, "rate" to rate, "isMuted" to isMuted, "volume" to volume, "transportState" to "TRANSITIONING" )) } // 使用辅助方法创建DIDL-Lite元数据 val didl = buildDIDLMetadata(url, title, mimeType) // 先尝试停止当前播放内容 try { stop() Log.i(TAG, "已停止当前播放内容") Thread.sleep(500) // 给设备一些处理时间 } catch (e: Exception) { Log.w(TAG, "停止播放内容时发生警告", e) // 继续执行,因为有些设备在未播放状态下会报错 } // 发送设置媒体URI请求 try { val setURIArgs = mapOf( "InstanceID" to "0", "CurrentURI" to url, "CurrentURIMetaData" to didl ) sendSoapRequest(controlURL, "urn:schemas-upnp-org:service:AVTransport:1", "SetAVTransportURI", setURIArgs) Log.i(TAG, "成功设置媒体URI") } catch (e: Exception) { Log.e(TAG, "设置媒体URI失败", e) // 通知前端错误 isBuffering = false mainHandler.post { sendEvent("onError", mapOf( "code" to "MEDIA_URI_ERROR", "message" to "设置媒体URI失败: ${e.message}" )) } throw Exception("设置媒体失败: ${e.message}") } // 等待一段时间让设备处理媒体 Thread.sleep(500) // 发送播放请求 try { val playArgs = mapOf( "InstanceID" to "0", "Speed" to "1" ) sendSoapRequest(controlURL, "urn:schemas-upnp-org:service:AVTransport:1", "Play", playArgs) Log.i(TAG, "成功开始播放") // 记录状态 transportState = "PLAYING" isBuffering = false // 启动状态定期更新 mainHandler.post { startStatusUpdates() } } catch (e: Exception) { Log.e(TAG, "播放失败", e) // 通知前端错误 isBuffering = false mainHandler.post { sendEvent("onError", mapOf( "code" to "PLAY_ERROR", "message" to "播放失败: ${e.message}" )) } throw Exception("播放失败: ${e.message}") } // 获取时长和其他信息 requestMediaInfo() } catch (e: Exception) { Log.e(TAG, "播放媒体失败", e) throw e } } fun pause() { try { val device = currentDevice ?: throw Exception("No device connected") val controlURL = device.optString("controlURL") if (controlURL.isBlank()) { throw Exception("Device has no control URL") } val pauseArgs = mapOf("InstanceID" to "0") sendSoapRequest(controlURL, "urn:schemas-upnp-org:service:AVTransport:1", "Pause", pauseArgs) transportState = "PAUSED_PLAYBACK" } catch (e: Exception) { Log.e(TAG, "Pause error", e) throw e } } fun resume() { try { val device = currentDevice ?: throw Exception("No device connected") val controlURL = device.optString("controlURL") if (controlURL.isBlank()) { throw Exception("Device has no control URL") } val playArgs = mapOf( "InstanceID" to "0", "Speed" to "1" ) sendSoapRequest(controlURL, "urn:schemas-upnp-org:service:AVTransport:1", "Play", playArgs) transportState = "PLAYING" } catch (e: Exception) { Log.e(TAG, "Resume error", e) throw e } } fun stop() { try { val device = currentDevice ?: return val controlURL = device.optString("controlURL") if (controlURL.isBlank()) { return } val stopArgs = mapOf("InstanceID" to "0") sendSoapRequest(controlURL, "urn:schemas-upnp-org:service:AVTransport:1", "Stop", stopArgs) transportState = "STOPPED" // 停止状态更新 mainHandler.post { stopStatusUpdates() } } catch (e: Exception) { Log.e(TAG, "Stop error", e) } } @SuppressLint("DefaultLocale") fun seek(position: Long) { try { val device = currentDevice ?: throw Exception("No device connected") val controlURL = device.optString("controlURL") if (controlURL.isBlank()) { throw Exception("Device has no control URL") } // 将位置格式化为DLNA时间格式 (HH:MM:SS) val hours = position / 3600 val minutes = (position % 3600) / 60 val seconds = position % 60 val formattedTime = String.format("%02d:%02d:%02d", hours, minutes, seconds) val seekArgs = mapOf( "InstanceID" to "0", "Unit" to "REL_TIME", "Target" to formattedTime ) sendSoapRequest(controlURL, "urn:schemas-upnp-org:service:AVTransport:1", "Seek", seekArgs) this.position = position } catch (e: Exception) { Log.e(TAG, "Seek error", e) throw e } } fun setVolume(volume: Int) { try { val device = currentDevice ?: throw Exception("No device connected") val renderControlURL = device.optString("renderControlURL") if (renderControlURL.isBlank()) { throw Exception("Device has no render control URL") } val safeVolume = volume.coerceIn(0, 100) val volumeArgs = mapOf( "InstanceID" to "0", "Channel" to "Master", "DesiredVolume" to safeVolume.toString() ) sendSoapRequest(renderControlURL, "urn:schemas-upnp-org:service:RenderingControl:1", "SetVolume", volumeArgs) this.volume = safeVolume } catch (e: Exception) { Log.e(TAG, "Set volume error", e) throw e } } fun getPlaybackStatus(): Map { try { requestPositionInfo() return mapOf( "isPlaying" to (transportState == "PLAYING"), "duration" to duration, "position" to position, "volume" to volume, // 新增属性 "rate" to rate, "isMuted" to isMuted, "isBuffering" to isBuffering, "transportState" to transportState ) } catch (e: Exception) { Log.e(TAG, "Get playback status error", e) throw e } } // 新增:获取缓冲状态 fun getBufferingStatus(): Map { return mapOf( "isBuffering" to isBuffering, "duration" to duration, "position" to position, "transportState" to transportState ) } // 新增:设置播放速率 fun setRate(newRate: Double) { try { val device = currentDevice ?: throw Exception("设备未连接") val controlURL = device.optString("controlURL") if (controlURL.isBlank()) { throw Exception("设备没有控制URL") } // 注意:DLNA标准不直接支持更改播放速率 // 这里我们仅记录速率状态变化,实际控制可能需要设备特定实现 Log.i(TAG, "设置播放速率: $newRate (注意:设备可能不支持)") // 如果是暂停状态且设置速率大于0,则恢复播放 if (transportState == "PAUSED_PLAYBACK" && newRate > 0) { resume() } // 记录新的速率值 rate = newRate } catch (e: Exception) { Log.e(TAG, "设置播放速率失败", e) throw e } } // 新增:设置静音状态 fun setMuted(muted: Boolean) { try { val device = currentDevice ?: throw Exception("设备未连接") val renderControlURL = device.optString("renderControlURL") if (renderControlURL.isBlank()) { // 无法直接控制音量,仅记录状态变化 Log.i(TAG, "设备没有音量控制URL,仅记录静音状态: $muted") isMuted = muted return } // 设置设备音量,0表示静音 val volumeLevel = if (muted) 0 else volume val volumeArgs = mapOf( "InstanceID" to "0", "Channel" to "Master", "DesiredVolume" to volumeLevel.toString() ) sendSoapRequest(renderControlURL, "urn:schemas-upnp-org:service:RenderingControl:1", "SetVolume", volumeArgs) isMuted = muted // 如果不是静音,恢复之前的音量 if (!muted && volume == 0) { // 默认音量为50 setVolume(50) } } catch (e: Exception) { Log.e(TAG, "设置静音状态失败", e) throw e } } private fun requestMediaInfo() { try { val device = currentDevice ?: return val controlURL = device.optString("controlURL") if (controlURL.isBlank()) { return } // 使用参数直接调用sendSoapRequest val mediaInfoArgs = mapOf("InstanceID" to "0") val response = sendSoapRequest(controlURL, "urn:schemas-upnp-org:service:AVTransport:1", "GetMediaInfo", mediaInfoArgs) // 使用DOM解析结果 val mediaDuration = response["MediaDuration"] if (mediaDuration != null && mediaDuration != "NOT_IMPLEMENTED") { duration = XMLUtils.parseTimeToSeconds(mediaDuration) Log.i(TAG, "媒体时长: $duration 秒") } val currentURI = response["CurrentURI"] if (currentURI != null && currentURI.isNotEmpty()) { Log.i(TAG, "当前媒体URI: $currentURI") } } catch (e: Exception) { Log.e(TAG, "获取媒体信息失败", e) } } private fun requestPositionInfo() { try { val device = currentDevice ?: return val controlURL = device.optString("controlURL") if (controlURL.isBlank()) { return } // 使用参数直接调用sendSoapRequest val positionInfoArgs = mapOf("InstanceID" to "0") val response = sendSoapRequest(controlURL, "urn:schemas-upnp-org:service:AVTransport:1", "GetPositionInfo", positionInfoArgs) // 直接从解析结果获取值 val relTime = response["RelTime"] if (relTime != null && relTime != "NOT_IMPLEMENTED") { position = XMLUtils.parseTimeToSeconds(relTime) Log.i(TAG, "当前播放位置: $position 秒") } val trackDuration = response["TrackDuration"] if (trackDuration != null && trackDuration != "NOT_IMPLEMENTED") { duration = XMLUtils.parseTimeToSeconds(trackDuration) Log.i(TAG, "轨道总时长: $duration 秒") } val trackURI = response["TrackURI"] if (trackURI != null && trackURI.isNotEmpty()) { Log.i(TAG, "当前轨道URI: $trackURI") } // 添加:检测缓冲状态 isBuffering = (transportState == "TRANSITIONING") // 获取传输状态 requestTransportInfo() } catch (e: Exception) { Log.e(TAG, "获取位置信息失败", e) } } private fun requestTransportInfo() { try { val device = currentDevice ?: return val controlURL = device.optString("controlURL") if (controlURL.isBlank()) { return } // 使用参数直接调用sendSoapRequest val transportInfoArgs = mapOf("InstanceID" to "0") val response = sendSoapRequest(controlURL, "urn:schemas-upnp-org:service:AVTransport:1", "GetTransportInfo", transportInfoArgs) // 直接从解析结果获取状态 val prevState = transportState val state = response["CurrentTransportState"] if (state != null && state.isNotEmpty()) { transportState = state Log.i(TAG, "当前传输状态: $transportState") // 检测播放完成 if (prevState == "PLAYING" && (transportState == "STOPPED" || transportState == "NO_MEDIA_PRESENT")) { // 播放可能已完成 Log.i(TAG, "检测到媒体播放可能已完成") // 通知前端播放完成 mainHandler.post { sendEvent("onPlaybackStatusChanged", mapOf( "isPlaying" to false, "isCompleted" to true, "duration" to duration, "position" to duration, // 设置位置为总时长,表示已到达末尾 "rate" to rate, "isMuted" to isMuted, "volume" to volume, "transportState" to transportState )) // 停止状态更新 stopStatusUpdates() } } } } catch (e: Exception) { Log.e(TAG, "获取传输信息失败", e) } } /** * 发送SOAP请求 * @param controlURL 控制URL * @param serviceType 服务类型 * @param actionName 动作名称 * @param arguments 参数 * @return 响应结果 */ private fun sendSoapRequest( controlURL: String, serviceType: String, actionName: String, arguments: Map ): Map { Log.i(TAG, "准备发送SOAP请求到: $controlURL") Log.i(TAG, "服务类型: $serviceType, 动作: $actionName") if (controlURL.isEmpty()) { Log.e(TAG, "控制URL为空,无法发送SOAP请求") throw Exception("控制URL不能为空") } var connection: HttpURLConnection? = null var outputStream: OutputStream? = null var inputStream: InputStream? = null try { // 构建SOAP请求体 val soapBody = buildSoapRequestBody(serviceType, actionName, arguments) // 创建HTTP连接 val url = URL(controlURL) connection = url.openConnection() as HttpURLConnection connection.requestMethod = "POST" connection.doOutput = true connection.setRequestProperty("Content-Type", "text/xml; charset=\"utf-8\"") connection.setRequestProperty("SOAPAction", "\"$serviceType#$actionName\"") connection.setRequestProperty("Connection", "close") connection.connectTimeout = 15000 // 15秒超时 connection.readTimeout = 15000 // 写入请求体 outputStream = connection.outputStream outputStream.write(soapBody.toByteArray()) // 获取响应 val responseCode = connection.responseCode Log.i(TAG, "SOAP请求响应码: $responseCode") if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_INTERNAL_ERROR) { // 即使是500错误,DLNA设备也可能返回有效的SOAP响应 inputStream = if (responseCode == HttpURLConnection.HTTP_OK) { connection.inputStream } else { connection.errorStream } val response = inputStream.bufferedReader().use { it.readText() } if (responseCode == HttpURLConnection.HTTP_INTERNAL_ERROR) { Log.w(TAG, "SOAP请求返回500错误: $response") // 解析SOAP错误 return parseSoapResponse(response).apply { // 添加错误标记 this["Error"] = "true" this["ErrorCode"] = extractValue(response, "errorCode") ?: "未知错误码" this["ErrorDescription"] = extractValue(response, "errorDescription") ?: "未知错误描述" } } Log.d(TAG, "SOAP响应: $response") return parseSoapResponse(response) } else { val errorMessage = "SOAP请求失败,响应码: $responseCode" Log.e(TAG, errorMessage) // 尝试从错误流读取更多信息 val errorInfo = connection.errorStream?.bufferedReader()?.use { it.readText() } ?: "没有错误详情" Log.e(TAG, "错误详情: $errorInfo") throw Exception(errorMessage) } } catch (e: Exception) { Log.e(TAG, "发送SOAP请求时出错", e) val errorMap = mutableMapOf() errorMap["Error"] = "true" errorMap["ErrorMessage"] = e.message ?: "未知错误" return errorMap } finally { // 关闭资源 try { outputStream?.close() } catch (e: IOException) { Log.e(TAG, "关闭输出流失败", e) } try { inputStream?.close() } catch (e: IOException) { Log.e(TAG, "关闭输入流失败", e) } try { connection?.disconnect() } catch (e: Exception) { Log.e(TAG, "断开连接失败", e) } } } private fun parseTimeToSeconds(time: String): Long { try { val parts = time.split(":") if (parts.size == 3) { val hours = parts[0].toLong() val minutes = parts[1].toLong() val seconds = parts[2].toLong() return hours * 3600 + minutes * 60 + seconds } } catch (e: Exception) { Log.e(TAG, "Time parsing error: $time", e) } return 0 } /** * 构建SOAP请求体 */ private fun buildSoapRequestBody(serviceType: String, actionName: String, arguments: Map): String { return """ ${arguments.entries.joinToString("\n") { "<${it.key}>${it.value}" }} """.trimIndent() } /** * 解析SOAP响应,使用DOM解析替代正则表达式 */ private fun parseSoapResponse(response: String): MutableMap { val result = mutableMapOf() try { // 添加原始响应用于调试 result["rawResponse"] = response // 使用DOM解析XML val doc = XMLUtils.parseXmlString(response) if (doc != null) { val root = doc.documentElement // 检查是否有SOAP Fault val faultElements = root.getElementsByTagName("Fault") if (faultElements.length > 0) { val fault = faultElements.item(0) if (fault is org.w3c.dom.Element) { val faultcode = XMLUtils.getElementTextContent(fault, "faultcode") val faultstring = XMLUtils.getElementTextContent(fault, "faultstring") result["Error"] = "true" result["ErrorCode"] = faultcode result["ErrorMessage"] = faultstring Log.e(TAG, "SOAP错误: $faultcode - $faultstring") return result } } // 查找Body元素 val bodyElements = root.getElementsByTagName("Body") if (bodyElements.length > 0) { val body = bodyElements.item(0) as org.w3c.dom.Element // 递归提取所有元素的文本内容 extractElementsToMap(body, result) } } else { // 如果DOM解析失败,回退到正则表达式方法作为备选 Log.w(TAG, "DOM解析SOAP响应失败,回退到正则表达式") val regex = "<([a-zA-Z0-9_]+)>(.*?)".toRegex() val matches = regex.findAll(response) for (match in matches) { val tagName = match.groupValues[1] val value = match.groupValues[2] result[tagName] = value } } } catch (e: Exception) { Log.e(TAG, "解析SOAP响应时出错", e) result["Error"] = "true" result["ErrorMessage"] = "解析响应失败: ${e.message}" } return result } /** * 递归提取DOM元素中的所有文本内容到Map */ private fun extractElementsToMap(element: org.w3c.dom.Element, map: MutableMap, prefix: String = "") { val childNodes = element.childNodes for (i in 0 until childNodes.length) { val node = childNodes.item(i) if (node is org.w3c.dom.Element) { val localName = node.localName ?: node.nodeName if (node.childNodes.length == 1 && node.firstChild.nodeType == org.w3c.dom.Node.TEXT_NODE) { // 如果元素只包含文本,直接添加到Map val textContent = node.textContent?.trim() ?: "" map[localName] = textContent Log.d(TAG, "解析SOAP元素: $localName = $textContent") } else { // 如果元素包含子元素,递归处理 extractElementsToMap(node, map, "$prefix$localName.") } } } } /** * 从XML中提取特定标签的值,使用DOM解析替代正则表达式 */ private fun extractValue(xml: String, tagName: String): String? { var inputStream: java.io.InputStream? = null try { // 尝试使用DOM解析 inputStream = xml.byteInputStream() val doc = XMLUtils.parseXmlString(xml) if (doc != null) { val elements = doc.getElementsByTagName(tagName) if (elements.length > 0) { return elements.item(0).textContent } } else { // 如果DOM解析失败,回退到正则表达式(保留作为备选) Log.w(TAG, "DOM解析XML失败,回退到正则表达式提取标签值: $tagName") val regex = "<$tagName>(.*?)".toRegex() val match = regex.find(xml) return match?.groupValues?.get(1) } } catch (e: Exception) { Log.e(TAG, "提取XML标签值出错: $tagName", e) // 回退到正则表达式 val regex = "<$tagName>(.*?)".toRegex() val match = regex.find(xml) return match?.groupValues?.get(1) } finally { try { inputStream?.close() } catch (e: Exception) { Log.e(TAG, "关闭输入流失败", e) } } return null } } private fun JSONObject.toMap(): Map { Log.d(TAG, "开始转换JSONObject到Map: $this") val map = mutableMapOf() val keys = this.keys() // 先检查是否存在controlURL并记录它 val controlURL = if (has("controlURL")) optString("controlURL") else null if (controlURL != null && controlURL.isNotEmpty()) { Log.i(TAG, "JSONObject中发现controlURL: $controlURL") } while (keys.hasNext()) { val key = keys.next() val value = this.opt(key) if (key == "controlURL") { Log.i(TAG, "处理controlURL,值为: $value") } try { when (value) { is JSONObject -> map[key] = value.toMap() is JSONArray -> { val list = mutableListOf() for (i in 0 until value.length()) { list.add(convertJsonValue(value.opt(i))) } map[key] = list } JSONObject.NULL -> map[key] = null else -> map[key] = value } } catch (e: Exception) { Log.e(TAG, "转换${key}时出错: ${e.message}") map[key] = if (value != null) value.toString() else null } } // 确保controlURL被包含 if (controlURL != null && controlURL.isNotEmpty() && !map.containsKey("controlURL")) { Log.w(TAG, "controlURL在JSONObject中存在但未被添加到Map,手动添加: $controlURL") map["controlURL"] = controlURL } // 处理特殊情况:设备对象缺少controlURL但有renderControlURL if (!map.containsKey("controlURL") && map.containsKey("renderControlURL")) { val renderControlURL = map["renderControlURL"] as? String if (renderControlURL != null && renderControlURL.isNotEmpty()) { Log.w(TAG, "设备没有controlURL但有renderControlURL,使用修改后的renderControlURL作为控制URL") // 通常renderControlURL和controlURL路径相似,只是服务名不同 // 尝试从renderControlURL构造可能的controlURL val possibleControlURL = renderControlURL.replace("RenderingControl", "AVTransport") map["controlURL"] = possibleControlURL Log.i(TAG, "基于renderControlURL推测的controlURL: $possibleControlURL") } } // 记录最终结果 Log.d(TAG, "JSONObject转换为Map完成。controlURL: ${map["controlURL"] ?: "不存在"}") return map } // 递归转换JSON值的辅助方法 private fun convertJsonValue(value: Any?): Any? { return when (value) { is JSONObject -> value.toMap() is JSONArray -> { val list = mutableListOf() for (i in 0 until value.length()) { list.add(convertJsonValue(value.opt(i))) } list } JSONObject.NULL -> null else -> value } } // Miracast管理器 @SuppressLint("NewApi") private inner class MiracastManager(private val appContext: AppContext) { private var mediaRouter: MediaRouter? = null private var displayManager: DisplayManager? = null private var mediaProjectionManager: MediaProjectionManager? = null private val miracastDevices = ConcurrentHashMap() private var currentRouteId: String? = null private var mediaRouterCallback: MediaRouter.Callback? = null private var currentProjectionMode: String = SCREEN_MIRRORING // 默认为屏幕镜像模式 init { try { mediaRouter = appContext.reactContext?.getSystemService(Context.MEDIA_ROUTER_SERVICE) as? MediaRouter displayManager = appContext.reactContext?.getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager mediaProjectionManager = appContext.reactContext?.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as? MediaProjectionManager Log.i(TAG, "Miracast管理器初始化: MediaRouter=${mediaRouter != null}, DisplayManager=${displayManager != null}, MediaProjectionManager=${mediaProjectionManager != null}") } catch (e: Exception) { Log.e(TAG, "Miracast管理器初始化失败", e) } } // 检查是否支持投屏 fun isProjectionSupported(): Boolean { return mediaRouter != null && displayManager != null && mediaProjectionManager != null } // 检查是否可以播放视频 fun canPlayVideo(): Boolean { // 检查当前投屏模式是否为视频模式 return isProjectionSupported() && currentProjectionMode == VIDEO_ONLY && isProjecting } // 播放视频 fun playVideo(url: String, title: String, mimeType: String): Boolean { if (!canPlayVideo()) { Log.e(TAG, "当前模式不支持视频播放,请使用VIDEO_ONLY模式") return false } try { Log.i(TAG, "尝试通过Miracast播放视频: $url") // 这里需要根据实际情况实现Miracast的视频播放 // 以下是一个示例实现,实际应用中可能需要根据设备特性调整 // 在某些设备上,可以通过发送Intent来播放视频 val intent = Intent(Intent.ACTION_VIEW) intent.setDataAndType(url.toUri(), mimeType) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) // 如果有mediaProjection,可以尝试通过它来播放 if (mediaProjection != null) { // 使用VirtualDisplay或其他方式播放视频 // 这部分需要根据具体需求和设备特性实现 Log.i(TAG, "使用MediaProjection播放视频") } else { // 尝试使用系统默认播放器 appContext.reactContext?.startActivity(intent) Log.i(TAG, "使用系统播放器播放视频") } return true } catch (e: Exception) { Log.e(TAG, "Miracast视频播放失败", e) return false } } // 开始搜索Miracast设备 fun startDiscovery(onDeviceFound: (device: JSONObject, isNew: Boolean) -> Unit) { if (!isProjectionSupported()) { Log.e(TAG, "当前设备不支持Miracast") return } if (mediaRouterCallback != null) { Log.i(TAG, "Miracast设备搜索已在进行中") return } try { Log.i(TAG, "开始搜索Miracast设备...") // 创建MediaRouter回调 mediaRouterCallback = object : MediaRouter.Callback() { override fun onRouteAdded(router: MediaRouter, route: MediaRouter.RouteInfo) { handleRouteChange(route, true, onDeviceFound) } override fun onRouteRemoved(router: MediaRouter, route: MediaRouter.RouteInfo) { val deviceId = getRouteId(route) if (miracastDevices.containsKey(deviceId)) { miracastDevices.remove(deviceId) mainHandler.post { sendEvent("onDeviceDisappeared", mapOf("deviceId" to deviceId)) } Log.i(TAG, "Miracast设备已移除: ${route.name} ($deviceId)") } } override fun onRouteChanged(router: MediaRouter, route: MediaRouter.RouteInfo) { handleRouteChange(route, false, onDeviceFound) } // 适用于API 26+的方法 override fun onRouteSelected(router: MediaRouter, type: Int, info: MediaRouter.RouteInfo) { currentRouteId = getRouteId(info) Log.i(TAG, "Miracast设备已选择: ${info.name} ($currentRouteId)") } // 适用于API 26+的方法 override fun onRouteUnselected(router: MediaRouter, type: Int, info: MediaRouter.RouteInfo) { if (getRouteId(info) == currentRouteId) { currentRouteId = null Log.i(TAG, "Miracast设备已取消选择: ${info.name}") } } // 必需实现的抽象方法 override fun onRouteGrouped(router: MediaRouter, info: MediaRouter.RouteInfo, group: MediaRouter.RouteGroup, index: Int) { Log.i(TAG, "Miracast路由已分组: ${info.name}") } override fun onRouteUngrouped(router: MediaRouter, info: MediaRouter.RouteInfo, group: MediaRouter.RouteGroup) { Log.i(TAG, "Miracast路由已解除分组: ${info.name}") } override fun onRouteVolumeChanged(router: MediaRouter, info: MediaRouter.RouteInfo) { Log.i(TAG, "Miracast路由音量已更改: ${info.name}") } } // 添加回调并选择合适的路由类型 mediaRouter?.addCallback( MediaRouter.ROUTE_TYPE_LIVE_VIDEO, mediaRouterCallback as MediaRouter.Callback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN ) // 初始扫描现有路由 scanExistingRoutes(onDeviceFound) Log.i(TAG, "Miracast设备搜索已启动") } catch (e: Exception) { Log.e(TAG, "启动Miracast设备搜索失败", e) } } // 扫描现有路由 private fun scanExistingRoutes(onDeviceFound: (device: JSONObject, isNew: Boolean) -> Unit) { try { // 在Android API 26+中,getRoutes()不可用,我们需要使用其他方式获取路由 val router = mediaRouter ?: return // 获取当前选择的路由 val selectedRoute = router.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_VIDEO) if (selectedRoute != null) { handleRouteChange(selectedRoute, true, onDeviceFound) Log.i(TAG, "扫描到当前选择的Miracast路由: ${selectedRoute.name}") } // 由于无法直接获取所有路由,我们将依赖onRouteAdded回调来发现设备 Log.i(TAG, "已设置路由发现回调,等待设备发现...") } catch (e: Exception) { Log.e(TAG, "扫描现有Miracast路由失败", e) } } // 处理路由变化 private fun handleRouteChange( route: MediaRouter.RouteInfo, isNew: Boolean, onDeviceFound: (device: JSONObject, isNew: Boolean) -> Unit ) { try { if (route.name == null || !isMiracastRoute(route)) { return } val deviceId = getRouteId(route) val existingDevice = miracastDevices[deviceId] val isReallyNew = existingDevice == null if (!isReallyNew && !isNew) { return // 如果设备已存在且不是新设备,跳过 } val deviceInfo = JSONObject().apply { put("id", deviceId) put("name", route.name) put("type", "miracast") put("model", "Miracast Display") put("manufacturer", "Wireless Display") // 保存路由名称以便后续使用 put("routeName", route.name) } miracastDevices[deviceId] = deviceInfo onDeviceFound(deviceInfo, isReallyNew) if (isReallyNew) { Log.i(TAG, "发现新的Miracast设备: ${route.name} ($deviceId)") } else { Log.i(TAG, "更新Miracast设备: ${route.name} ($deviceId)") } // 添加设备到总设备列表 addOrUpdateDevice(deviceInfo) } catch (e: Exception) { Log.e(TAG, "处理Miracast路由变化失败", e) } } // 停止设备搜索 fun stopDiscovery() { try { if (mediaRouterCallback != null && mediaRouter != null) { mediaRouter?.removeCallback(mediaRouterCallback as MediaRouter.Callback) mediaRouterCallback = null Log.i(TAG, "已停止Miracast设备搜索") } } catch (e: Exception) { Log.e(TAG, "停止Miracast设备搜索失败", e) } } // 开始投屏 fun startProjection(device: JSONObject, mode: String): Boolean { if (!isProjectionSupported()) { Log.e(TAG, "当前设备不支持Miracast投屏") return false } try { val deviceId = device.optString("id") val deviceName = device.optString("name") if (deviceName.isEmpty()) { Log.e(TAG, "设备没有有效的名称") return false } // 根据投屏模式设置不同的参数 val projectionMode = if (mode == VIDEO_ONLY) { Log.i(TAG, "使用视频模式投屏") VIDEO_ONLY } else { Log.i(TAG, "使用屏幕镜像模式投屏") SCREEN_MIRRORING } // 更新当前投屏模式 currentProjectionMode = projectionMode Log.i(TAG, "已设置投屏模式: $currentProjectionMode") // 选择路由 - 直接使用设备ID来标识 // 由于我们无法遍历所有路由,我们依赖onRouteAdded回调中已注册的设备 // 而现在只能选择已经选择的路由或使用默认路由选择器 // 通知MediaRouter我们要提供一个路由选择器 Log.i(TAG, "开始选择Miracast路由: $deviceName, 投屏模式: $projectionMode") // 设置currentRouteId以跟踪当前投屏状态 currentRouteId = deviceId // 通知用户需要手动选择设备 Log.i(TAG, "请在系统投屏对话框中选择设备: $deviceName") // 储存当前投屏模式,以便后续使用 val projectionPrefs = appContext.reactContext?.getSharedPreferences( "MiracastProjectionPrefs", Context.MODE_PRIVATE ) projectionPrefs?.edit()?.apply { putString("lastMode", projectionMode) apply() } // 这里我们实际上无法直接选择特定的路由,因为我们不能获取所有路由列表 // 我们需要依赖系统的路由选择器UI // 在真实场景中,你需要集成系统的MediaRouteChooserDialog return true } catch (e: Exception) { Log.e(TAG, "启动Miracast投屏失败", e) return false } } // 停止投屏 fun stopProjection() { try { if (currentRouteId != null) { // 使用默认路由 mediaRouter?.getDefaultRoute()?.let { mediaRouter?.selectRoute( MediaRouter.ROUTE_TYPE_LIVE_VIDEO, it ) } currentRouteId = null Log.i(TAG, "已停止Miracast投屏") } } catch (e: Exception) { Log.e(TAG, "停止Miracast投屏失败", e) } } // 通过ID查找路由 private fun findRouteById(routeId: String): MediaRouter.RouteInfo? { // 在API 26+中,getRoutes()不可用 // 我们使用已经缓存在miracastDevices中的设备信息来查找 val router = mediaRouter ?: return null // 首先尝试找到缓存的设备信息 val device = miracastDevices.values.find { it.optString("id") == routeId } if (device != null) { val deviceName = device.optString("name") Log.i(TAG, "在缓存中找到设备: $deviceName") // 然后检查当前选择的路由是否匹配名称 val selectedRoute = router.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_VIDEO) if (selectedRoute != null && selectedRoute.name == deviceName) { Log.i(TAG, "当前选择的路由匹配缓存的设备") return selectedRoute } // 如果当前选择的路由不匹配,返回null // 实际使用时需要选择路由,这将在startProjection方法中处理 return null } Log.e(TAG, "找不到路由ID: $routeId") return null } // 判断是否为Miracast路由 private fun isMiracastRoute(route: MediaRouter.RouteInfo): Boolean { // 排除默认路由 if (mediaRouter?.getDefaultRoute() == route) { return false } // 检查路由类型是否支持实时视频 if ((route.supportedTypes and MediaRouter.ROUTE_TYPE_LIVE_VIDEO) == 0) { return false } // 忽略有线显示设备 val nameStr = route.name?.toString() ?: "" val name = nameStr.lowercase() if (name.contains("hdmi") || name.contains("wired")) { return false } return true } // 获取路由的唯一ID private fun getRouteId(route: MediaRouter.RouteInfo): String { val nameObj = route.name ?: "unknown" val nameStr = nameObj.toString() // 生成唯一ID return "miracast-" + UUID.nameUUIDFromBytes(nameStr.toByteArray(Charsets.UTF_8)).toString() } } // 辅助函数:分割XML中的service标签 private fun splitXmlServiceTags(serviceListXml: String): List { val services = mutableListOf() var currentIndex = 0 while (true) { val serviceStartIndex = serviceListXml.indexOf("", serviceStartIndex) if (serviceEndIndex == -1) break val serviceXml = serviceListXml.substring(serviceStartIndex, serviceEndIndex + 10) // 10是""的长度 services.add(serviceXml) currentIndex = serviceEndIndex + 10 } return services } } // 添加XML工具类 private class XMLUtils { companion object { // 从DOM节点提取文本内容 fun getElementTextContent(element: org.w3c.dom.Element?, tagName: String): String { try { if (element == null) return "" val nodeList = element.getElementsByTagName(tagName) if (nodeList.length > 0) { val node = nodeList.item(0) return node?.textContent ?: "" } } catch (e: Exception) { Log.e(TAG, "提取XML标签内容失败: $tagName", e) } return "" } // 解析SOAP响应中的错误信息 fun parseSoapError(response: String): Pair? { try { if (!response.contains("")) return null val errorCodeMatch = Regex("(\\d+)").find(response) val errorDescMatch = Regex("(.*?)").find(response) val errorCode = errorCodeMatch?.groupValues?.get(1) ?: "未知" val errorDesc = errorDescMatch?.groupValues?.get(1) ?: "未知错误" return Pair(errorCode, errorDesc) } catch (e: Exception) { Log.e(TAG, "解析SOAP错误失败", e) return null } } // 安全地创建XML文档构建器,禁用外部实体解析防止XXE攻击 fun createSecureDocumentBuilder(): javax.xml.parsers.DocumentBuilder { val factory = javax.xml.parsers.DocumentBuilderFactory.newInstance() factory.isNamespaceAware = true // 启用命名空间支持 try { // 禁用外部实体解析,防止XXE攻击 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) factory.setFeature("http://xml.org/sax/features/external-general-entities", false) factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false) } catch (e: Exception) { Log.w(TAG, "无法设置XML安全特性", e) } return factory.newDocumentBuilder() } // 使用DOM解析XML字符串 fun parseXmlString(xml: String): org.w3c.dom.Document? { var inputStream: java.io.InputStream? = null try { val builder = createSecureDocumentBuilder() inputStream = xml.byteInputStream() return builder.parse(inputStream) } catch (e: Exception) { Log.e(TAG, "解析XML字符串失败", e) return null } finally { try { inputStream?.close() } catch (e: Exception) { Log.e(TAG, "关闭输入流失败", e) } } } // 使用DOM解析XML输入流 fun parseXmlStream(inputStream: java.io.InputStream): org.w3c.dom.Document? { try { val builder = createSecureDocumentBuilder() return builder.parse(inputStream) } catch (e: Exception) { Log.e(TAG, "解析XML输入流失败", e) return null } // 注意:这里不关闭inputStream,因为它由调用者管理 } // 提取指定元素下的所有子元素 fun getChildElements(parent: org.w3c.dom.Element?, tagName: String): List { val result = mutableListOf() try { if (parent == null) return result val nodeList = parent.getElementsByTagName(tagName) for (i in 0 until nodeList.length) { val node = nodeList.item(i) if (node is org.w3c.dom.Element) { result.add(node) } } } catch (e: Exception) { Log.e(TAG, "获取子元素失败: $tagName", e) } return result } // 格式化DLNA持续时间为秒数 fun parseTimeToSeconds(time: String): Long { try { if (time == "NOT_IMPLEMENTED" || time.isEmpty()) return 0 val parts = time.split(":") if (parts.size == 3) { val hours = parts[0].toLongOrNull() ?: 0 val minutes = parts[1].toLongOrNull() ?: 0 val seconds = parts[2].toLongOrNull() ?: 0 return hours * 3600 + minutes * 60 + seconds } } catch (e: Exception) { Log.e(TAG, "解析时间格式失败: $time", e) } return 0 } // 从秒数格式化为DLNA时间格式 (HH:MM:SS) fun formatSecondsToTime(seconds: Long): String { val hours = seconds / 3600 val minutes = (seconds % 3600) / 60 val secs = seconds % 60 return String.format("%02d:%02d:%02d", hours, minutes, secs) } } }