DLNA (UPnP AV-ஐ அடிப்படையாகக் கொண்டது) என்பது ஸ்மார்ட் டிவிகளில் உள்ள "Cast to TV" பொத்தான்களின் பின்னால் உள்ள நெறிமுறை ஆகும். இது முற்றிலும் உள்ளூர் நெட்வொர்க்கில் இயங்குகிறது, கிளவுட் கணக்கோ அல்லது இணைப்பு படிமுறையோ தேவையில்லை. PlainApp இதன் இரண்டு திசைகளையும் செயல்படுத்துகிறது: இது ஒரு உள்ளூர் வீடியோ, பாடல் அல்லது புகைப்படத்தை எந்த DLNA-இணக்கமான ஸ்மார்ட் டிவிக்கும் அனுப்ப முடியும், மேலும் தொலைபேசியையே ஒரு MediaRenderer ஆக மாற்றி, ஒரு டிவி ரிமோட் ஆப், VLC அல்லது மற்றொரு PlainApp அதற்கு காஸ்ட் செய்ய அனுமதிக்கிறது.
இந்த கட்டுரை வயர் நெறிமுறை (SSDP + SOAP + DIDL-Lite), டிவிகள் உண்மையில் தேவைப்படும் ஹெடர்களுடன் மீடியாவை ஸ்ட்ரீம் செய்யும் உள்ளூர் HTTP சர்வர், பிளேபேக் நிலைக்கான GENA நிகழ்வு சந்தா, மற்றும் அங்கீகாரம் இல்லாத 1990-கள் நெறிமுறையை நவீன தொலைபேசியில் பாதுகாப்பாக இயக்க உதவும் அனுப்புநர்-IP நம்பிக்கை மாதிரி ஆகியவற்றை உள்ளடக்கியது.
பொருளடக்கம்
- பொருளடக்கம்
- உயர்-நிலை கட்டமைப்பு
- SSDP கண்டுபிடிப்பு: சர்வர் இல்லாமல் சாதனங்களைக் கண்டறிதல்
- AVTransport கட்டுப்பாடு: SOAP நெறிமுறை
- DIDL-Lite மெட்டாடேட்டா மற்றும் இரட்டை-தப்பிக்கும் வினோதம்
- டிவிக்கு மீடியாவை வழங்குதல்: Range கோரிக்கைகள் மற்றும் DLNA ஹெடர்கள்
- GENA நிகழ்வுகள்: பிளேபேக் நிலை அழைப்புகள்
- பெறுநர் முறை: UPnP MediaRenderer ஆக மாறுதல்
- பாதுகாப்பு: அனுமதி/மறுப்பு பட்டியல்கள் வழியாக அனுப்புநர் நம்பிக்கை
- தளப் பிரிப்பு: commonMain ஒருங்கிணைப்பு, androidMain சாக்கெட்டுகள்
- தூய-Kotlin பொறியியல் குறிப்புகள்
- வடிவமைப்பு முறைகள் மீள்பார்வை
- மேலும் படிக்க
உயர்-நிலை கட்டமைப்பு
DLNA/UPnP AV-க்கு மைய சர்வர் அல்லது கிளவுட் கூறு எதுவும் இல்லை — அனைத்தும் உள்ளூர்-நெட்வொர்க் UDP மல்டிகாஸ்ட் (கண்டுபிடிப்பு) மற்றும் HTTP (கட்டுப்பாடு + மீடியா) வழியாக நடக்கிறது. PlainApp ஒரே commonMain நெறிமுறை குறியீட்டைப் பகிரும் இரண்டு சுயாதீன பாத்திரங்களை செயல்படுத்துகிறது:
| பாத்திரம் | செயல்பாடு | முக்கிய வகுப்புகள் |
|---|---|---|
| அனுப்புநர் ("Cast to TV") | ரெண்டரர்களை ஸ்கேன் செய்து, ஒன்றுக்கு URL-ஐப் பெறச் சொல்கிறது, பிளேபேக்கைக் கட்டுப்படுத்துகிறது | DlnaDeviceScanner, DlnaTransportController, CastPlayer |
| பெறுநர் ("Wireless Cast") | தன்னை MediaRenderer ஆக அறிவித்து, எந்த UPnP கட்டுப்படுத்தியிடமிருந்தும் கட்டுப்பாட்டை ஏற்கிறது | DlnaReceiverEngine, DlnaHttpRouter, DlnaSoapHandler, DlnaReceiverViewModel |
இரண்டு பாத்திரங்களும் features/dlna/common/-இலிருந்து DlnaSoap (SOAP உறை கட்டமைப்பிகள்) மற்றும் DlnaDevice (UPnP சாதன மாதிரி) ஆகியவற்றை மீண்டும் பயன்படுத்துகின்றன. DLNA விவரக்குறிப்பில் எந்த வகையான அங்கீகாரமும் இல்லை — LAN-இல் எவருக்கும் ஒரு ரெண்டரரின் கட்டுப்பாட்டு URL தெரிந்தால் அதற்கு கட்டளைகளை அனுப்ப முடியும். இது கீழே விவரிக்கப்பட்டுள்ள கிட்டத்தட்ட ஒவ்வொரு வடிவமைப்பு முடிவையும், குறிப்பாக பெறுநரின் நம்பிக்கை மாதிரியை வடிவமைக்கிறது.
SSDP கண்டுபிடிப்பு: சர்வர் இல்லாமல் சாதனங்களைக் கண்டறிதல்
கண்டுபிடிப்பு SSDP (Simple Service Discovery Protocol) ஐப் பயன்படுத்துகிறது, இது UDP மல்டிகாஸ்ட் 239.255.255.250:1900-க்கு மேல் ஒரு மெல்லிய அடுக்கு ஆகும். இங்கு கோப்பக சர்வர் எதுவும் இல்லை — சாதனங்கள் தங்களைத் தாங்களே அறிவித்து, தேடல் கேள்விகளுக்கு நேரடியாக பதிலளிக்கின்றன.
அனுப்புநராக, DlnaDeviceScanner urn:schemas-upnp-org:service:AVTransport:1 ஐ இலக்காகக் கொண்ட M-SEARCH தரவுத் துண்டை ஒளிபரப்பி, ஒவ்வொன்றும் ரெண்டரரின் description.xml-ஐ சுட்டிக்காட்டும் LOCATION ஹெடரைக் கொண்ட யூனிகாஸ்ட் 200 OK பதில்களைச் சேகரிக்கிறது. ஸ்கேனர் hostAddress மூலம் நகல்களை நீக்குகிறது — இது சாதன XML ஐ தானாக பாகுபடுத்தாது; அது CastViewModel.searchAsync()-க்கு விடப்படுகிறது, இது LOCATION-ஐப் பெற்று, device.update(xml)-ஐ அழைத்து, device.isAVTransport() உண்மையாக இருக்கும் சாதனங்களை மட்டுமே மேற்பரப்பில் காட்டுகிறது:
fun search(): Flow<DlnaDevice> = searchDlnaDevicesRaw().transform { ssdp ->
if (devices.none { it.hostAddress == ssdp.hostAddress }) {
val device = DlnaDevice(ssdp.hostAddress, ssdp.header)
devices.add(device)
emit(device)
}
}
பெறுநராக, DlnaReceiverEngine.runSsdpLoop() தொடக்கத்தில் மூன்று NOTIFY ssdp:alive தரவுத் துண்டுகளை அனுப்புகிறது (ரூட் சாதனம், MediaRenderer:1 சாதன வகை, AVTransport:1 சேவை வகை), ஒவ்வொரு 30 வினாடிகளுக்கும் மீண்டும் அறிவிக்கிறது (CACHE-CONTROL: max-age=1800), மற்றும் உள்வரும் M-SEARCH கோரிக்கைகளுக்கு யூனிகாஸ்ட் பதில்களுடன் பதிலளிக்கிறது. stop()-இல், 30 நிமிட கேச் காலாவதியாகும் வரை காத்திருக்காமல் உடனடியாக ssdp:byebye அனுப்புகிறது — எனவே "Wireless Cast" அணைக்கப்பட்ட உடனேயே ஒரு டிவி ரிமோட் ஆப் PlainApp-ஐ பட்டியலிடுவதை நிறுத்துகிறது.
போர்ட் தேர்வு மாற்று
பெறுநரின் HTTP சர்வர் முதலில் 7878 போர்ட்டை முயற்சிக்கிறது, பிறகு 7879, பிறகு 7880, கடைசியாக வேலை செய்த போர்ட்டை விரும்புகிறது:
private val CANDIDATE_PORTS = listOf(7878, 7879, 7880)
private fun openServerSocket(): DlnaServerSocket? {
val candidates = lastPort
?.let { listOf(it) + CANDIDATE_PORTS.filter { p -> p != it } }
?: CANDIDATE_PORTS
for (port in candidates) {
val ss = createDlnaServerSocket(port)
if (ss != null) return ss
}
return null
}
மூன்று போர்ட்களும் ஆக்கிரமிக்கப்பட்டிருந்தால் (அரிதானது, ஆனால் மற்ற DLNA ஆப்கள் இயங்கும்போது சாத்தியம்), startError அமைக்கப்பட்டு UI-இல் காண்பிக்கப்படுகிறது, அமைதியாக தோல்வியடைவதற்கு பதிலாக.
AVTransport கட்டுப்பாடு: SOAP நெறிமுறை
ஒரு ரெண்டரர் கண்டுபிடிக்கப்பட்டதும், பிளேபேக் UPnP AVTransport மூலம் கட்டுப்படுத்தப்படுகிறது, இது SOAP-over-HTTP சேவை ஆகும். ஒவ்வொரு செயலும் ரெண்டரரின் கட்டுப்பாட்டு URL-க்கு SOAPAction ஹெடர் மற்றும் SOAP உறையில் மூடப்பட்ட XML உடலுடன் கூடிய POST ஆகும்.
DlnaTransportController ஒரு பகிரப்பட்ட உதவியாளருடன் ஒவ்வொரு கோரிக்கையையும் கட்டமைக்கிறது:
private suspend fun executeAVTransportCommand(
device: DlnaDevice,
action: String,
parameters: String = "<InstanceID>0</InstanceID>",
): String {
val st = device.getAVTransportService()?.serviceType ?: return ""
return executeSOAPRequest(device, action, "<u:$action xmlns:u=\"$st\">$parameters</u:$action>")
}
executeSOAPRequest SOAPAction: "<serviceType>#<action>" ஐ அமைத்து DlnaSoap.requestEnvelope(soapBody)-ஐ POST செய்கிறது — பெறுநர் பக்கம் பதில்களை உருவாக்க பயன்படுத்தும் அதே உறை மாறிலிகள், எனவே வயர் வடிவமைப்பை commonMain-இல் ஒருமுறை மட்டுமே வரையறுக்க வேண்டும்.
பெறுநரின் DlnaHttpRouter.handleSoap() மறு முனையில் இதை பிரதிபலிக்கிறது: இது soapaction ஹெடரைப் படித்து, #-க்குப் பின் செயல் பெயரைப் பிரித்தெடுத்து, அதன் அடிப்படையில் அனுப்புகிறது — SetAVTransportURI, Play, Pause, Stop, Seek, GetTransportInfo, GetPositionInfo, GetMediaInfo, GetDeviceCapabilities. RenderingControl (ஒலி அளவு) நிலையான 100 உடன் மாற்றாக வைக்கப்பட்டுள்ளது — PlainApp UPnP வழியாக சாதன ஒலி அளவை வெளிப்படுத்தவில்லை.
DIDL-Lite மெட்டாடேட்டா மற்றும் இரட்டை-தப்பிக்கும் வினோதம்
SetAVTransportURI இரண்டு அளவுருக்களைக் கொண்டு செல்கிறது: CurrentURI (மீடியா URL) மற்றும் CurrentURIMetaData — தலைப்பு, மீடியா வகுப்பு மற்றும் ஆல்பம் கலையை விவரிக்கும் DIDL-Lite XML பகுதி, வெளிப்புற SOAP உடலுக்குள் XML-தப்பித்த சரமாக உட்பொதிக்கப்பட்டுள்ளது:
private fun buildDidlLiteMetadata(mediaUrl: String, title: String, albumArtUri: String): String {
val upnpClass = when {
ext in setOf("mp3", "m4a", "flac", ...) -> "object.item.audioItem.musicTrack"
ext in setOf("jpg", "jpeg", "png", ...) -> "object.item.imageItem"
else -> "object.item.videoItem"
}
val didl = """<DIDL-Lite xmlns="..."><item id="0" parentID="-1" restricted="0">
<dc:title>$escapedTitle</dc:title><upnp:class>$upnpClass</upnp:class>$albumArtTag</item></DIDL-Lite>"""
return didl.replace("&", "&").replace("<", "<").replace(">", ">")
}
DIDL-Lite XML இரண்டு முறை தப்பிக்கப்படுகிறது: ஒருமுறை தலைப்பு உரைக்காகவே (Fire & Ice என்ற பாடல் DIDL-Lite குறிச்சொற்களை உடைக்காமல் இருக்க), மற்றும் ஒருமுறை முழு DIDL-Lite ஆவணத்திற்காகவே (அதன் சொந்த </> குறிகள் அது உரையாக உட்பொதிக்கப்பட்டுள்ள வெளிப்புற SOAP உறையை உடைக்காமல் இருக்க). இது நன்கு அறியப்பட்ட UPnP வினோதமாகும், பிழை அல்ல — CurrentURIMetaData என்பது உரை உள்ளடக்கமாக வரையறுக்கப்பட்டுள்ளது, உட்பொதிக்கப்பட்ட XML உறுப்புகளாக அல்ல.
பெறுநர் பக்கத்தில், DlnaSoapHandler இதை மாற்றியமைக்கிறது: parseSoapAction SOAP உடலை ஒருமுறை தப்பிக்கவிலக்கி DIDL-Lite உரையைப் பெறுகிறது, பிறகு extractTitleFromDidlMeta/extractMediaTypeFromDidlMeta/extractAlbumArtUriFromDidlMeta ஒவ்வொன்றும் அந்த உள் சரத்தில் இரண்டாவது முறையாக நிறுவன தப்பிக்கவிலக்கம் மற்றும் குறிச்சொல் பிரித்தெடுப்பை செய்கின்றன:
fun extractMediaTypeFromDidlMeta(meta: String, fallbackUri: String = ""): DlnaMediaType {
val cls = meta.substring(classStart + 12, classEnd).lowercase()
return when {
"audioitem" in cls || "musictrack" in cls -> DlnaMediaType.AUDIO
"imageitem" in cls || "photo" in cls -> DlnaMediaType.IMAGE
"videoitem" in cls -> DlnaMediaType.VIDEO
else -> DlnaMediaType.UNKNOWN
}
}
<upnp:class> இல்லாவிட்டால் (சில அனுப்புநர்கள் அதைத் தவிர்க்கின்றனர்), cleanMediaTitle() URI-யின் கோப்பு நீட்சிக்கு மாற்றாகச் செல்கிறது — மீடியா வகை கண்டறிதல் ஒருபோதும் கடுமையாக தோல்வியடையாது, அது UNKNOWN ஆக தரமிறக்கப்படுகிறது, இது பாதுகாப்பான இயல்புநிலையாக வீடியோ பிளேயருக்கு வழியனுப்பப்படுகிறது.
டிவிக்கு மீடியாவை வழங்குதல்: Range கோரிக்கைகள் மற்றும் DLNA ஹெடர்கள்
SetAVTransportURI அழைப்பு ரெண்டரருக்கு மீடியாவை எங்கிருந்து பெறுவது என்பதை மட்டுமே சொல்கிறது — உண்மையான பைட்டுகள் PlainApp-இன் சொந்த உள்ளூர் HTTP சர்வரால், /media/{id}-இல் வழங்கப்படுகின்றன.
UrlHelper.getMediaHttpUrl(path) உண்மையான பாதையை (இது ஒரு content:// URI, ரிமோட் URL, அல்லது வெற்று கோப்பு பாதையாக இருக்கலாம்) ஒரு குறுகிய id-யின் கீழ் பதிவு செய்து http://<சாதன-ip>:<போர்ட்>/media/<id>.<ext>-ஐ வழங்குகிறது. பின்னர் வழி அது உண்மையில் எந்த வகை மூலமாக உள்ளது என்பதில் கிளைபிரிகிறது:
when {
path.isUrl() -> call.proxyUrl(path) // ரிமோட் URL: மேல்நிலை பதிலை ஸ்ட்ரீம் செய்க
isContentUri(path) -> call.respondStream { sink -> streamContentUri(path, sink) }
path.isImageFast() -> call.respondFile(path) // படங்கள்: நிலையான வழங்கல்
else -> call.respondDlnaFile(path) // ஆடியோ/வீடியோ: DLNA-அறிந்த வழங்கல்
}
respondDlnaFile என்பது சுவாரஸ்யமான வழக்கு — பல ஸ்மார்ட் டிவிகள் மற்றும் DLNA ரெண்டரர்கள் சரியான DLNA மீடியா சர்வர் பதில் போல் தெரியாவிட்டால் ஸ்ட்ரீமை இயக்க மறுக்கின்றன:
override suspend fun respondDlnaFile(path: String): Boolean {
val file = java.io.File(path)
if (!file.exists()) return false
applicationCall.response.run {
header("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*")
header("contentFeatures.dlna.org", "")
header("transferMode.dlna.org", "Streaming")
header("Connection", "keep-alive")
header("Server", "DLNADOC/1.50 UPnP/1.0 Plain/1.0 Android/${android.os.Build.VERSION.RELEASE}")
status(HttpStatusCode.PartialContent) // சில TV OS-கள் 206 ஐ மட்டுமே ஏற்கின்றன
}
applicationCall.respond(LocalFileContent(file))
return true
}
நிலை எப்போதும் 206 Partial Content ஆகவே உள்ளது, 200 OK அல்ல — சில டிவி ஃபார்ம்வேர் ஒரு வெற்று 200 பதிலை "தேட முடியாதது" என கருதி இயக்க மறுக்கிறது, முழு-கோப்பு GET-க்கு கூட. இந்த ஒற்றை நிலை-குறியீடு தேர்வு, பல உண்மையான சாதனங்களில் "நன்றாக இயங்குகிறது" மற்றும் "டிவி எப்போதும் சுழல்கிறது" ஆகியவற்றுக்கு இடையேயான வித்தியாசமாகும்.
அதே வழி காஸ்ட் ஆடியோ உருப்படிகளுக்கான ஆல்பம் கலையையும் வழங்குகிறது: UrlHelper.getAlbumArtHttpUrl() ஒரு content://media/.../albumart/<id> URI-ஐ ஒரே /media/{id} பாதையில் மேப்பிங் செய்கிறது, எனவே "மீடியா" என்பது பாடலா அல்லது அதன் அட்டைப் படமா என்பதைப் பொருட்படுத்தாமல் content:// ஸ்ட்ரீமிங் மற்றும் DLNA கோப்பு வழங்கல் ஒரே குறியீடு பாதையைப் பகிர்ந்து கொள்கின்றன.
GENA நிகழ்வுகள்: பிளேபேக் நிலை அழைப்புகள்
பிளேபேக்கைத் தொடங்கிய பிறகு, அனுப்புநர் ரெண்டரரின் AVTransport நிகழ்வு சேவைக்கு (GENA — General Event Notification Architecture) சந்தா செலுத்துகிறது, இதனால் கணிப்பு இல்லாமல் நிலை மாற்றங்களை அறிய முடியும்:
suspend fun subscribeEvent(device: DlnaDevice, callbackUrl: String): String {
val service = device.getAVTransportService() ?: return ""
val response = createHttpClient().subscribe(baseUrl + eventSubURL) {
headers { set("NT", "upnp:event"); set("TIMEOUT", "Second-3600"); set("CALLBACK", "<$callbackUrl>") }
}
return response.headers["SID"].orEmpty()
}
SUBSCRIBE/RENEW/UNSUBSCRIBE என்பவை தனிப்பயன் HTTP முறைகள் (நிலையான வினைச் சொற்களில் இல்லை), Ktor-இன் பொதுவான HttpMethod("SUBSCRIBE") கோரிக்கை கட்டமைப்பி மூலம் கையாளப்படுகின்றன. பின்னர் ரெண்டரர் callbackUrl-க்கு — PlainApp-இன் சொந்த /callback/cast வழி — போக்குவரத்து நிலை, நிலை அல்லது கால அளவு மாறும்போதெல்லாம் NOTIFY அனுப்புகிறது:
if (xml.contains("TransportState val=\"STOPPED\"") && !xml.contains("AVTransportURIMetaData")) {
// பிளேலிஸ்ட்டின் அடுத்த உருப்படிக்கு முன்னேறுக
} else if (xml.contains("TransportState val=\"PLAYING\"")) {
CastPlayer.isPlaying.value = true
}
நகல்-அழைப்பு பாதுகாப்பு
சில ரெண்டரர்கள் ஒரே STOPPED மாற்றத்திற்காக இரண்டு NOTIFY அழைப்புகளை விரைவாக அனுப்புகின்றன — இரண்டாவது தற்செயலாக AVTransportURIMetaData-ஐ கொண்டு செல்கிறது, முதலாவது இல்லை. இரண்டிலும் பிளேலிஸ்ட்டை முன்னேற்றுவது ஒவ்வொரு முறை பிளேபேக் இயற்கையாக நிறுத்தப்படும்போதும் ஒரு டிராக்கைத் தவிர்க்கும். !xml.contains("AVTransportURIMetaData") சரிபார்ப்பு ஒரு வேண்டுமென்றே, குறுகிய வடிப்பானாகும்: மெட்டாடேட்டா இல்லாத முதல் STOPPED அறிவிப்பு மட்டுமே தானியங்கி-முன்னேற்றத்தைத் தூண்டுகிறது. startPositionUpdater() வேலை ஒவ்வொரு வினாடியும் GetPositionInfo-ஐ கணிப்பாக வினவுகிறது, ஏனெனில் PlainApp-இன் சொந்த SUBSCRIBE ஒப்புகை மற்ற கட்டுப்படுத்திகளுக்கு நிகழ்வுகளைத் தள்ளாது — அனுப்புநர் பக்கம் மட்டுமே டிவிகளிடமிருந்து GENA அழைப்புகளை நுகருகிறது.
பெறுநர் முறை: UPnP MediaRenderer ஆக மாறுதல்
திசையை மாற்றவும்: எந்த DLNA கட்டுப்படுத்தியும் (டிவி ரிமோட் ஆப், VLC, மற்றொரு PlainApp) தொலைபேசிக்கே மீடியாவைத் தள்ள முடியும். DlnaReceiverEngine மேலே விவரிக்கப்பட்ட அதே வகையான HTTP + SSDP சர்வரைத் திறக்கிறது, ஆனால் கட்டுப்படுத்தியாக இல்லாமல் கட்டுப்படுத்தப்படும் MediaRenderer ஆக.
DlnaHttpRouter.route() description.xml-ஐ வழங்குகிறது (DlnaXmlTemplates.deviceDescription() ஆல் கட்டமைக்கப்பட்டது, ஒரு AVTransport மற்றும் ஒரு மாற்று RenderingControl சேவையை பட்டியலிடுகிறது) மற்றும் SOAP செயல்களை DlnaSoapHandler-க்கு அனுப்புகிறது. ஒரு SetAVTransportURI அழைப்பு உடனடியாக எதையும் இயக்காது — அது ஒரு PendingCastRequest-ஐ சேமித்து காத்திருக்கிறது:
if (uri.isNotEmpty()) {
DlnaRendererState.rawPendingCastRequest.value =
PendingCastRequest(senderIp, senderName, uri, title, mediaType, albumArtUri)
DlnaRendererState.pendingPlayQueued.value = false
}
நிலுவையில் உள்ள கோரிக்கை தீர்க்கப்படுவதற்கு முன் வரும் ஒரு Play பிளேபேக்கைத் தொடங்காது — அது pendingPlayQueued = true ஐ அமைக்கிறது, இதனால் காஸ்ட் கோரிக்கை ஏற்கப்பட்டதும் கட்டளை தானாகவே மீண்டும் இயங்குகிறது, அமைதியாக இழக்கப்படுவதற்கு பதிலாக:
val hasPending = DlnaRendererState.rawPendingCastRequest.value != null ||
DlnaRendererState.pendingCastRequest.value != null
if (hasPending) {
DlnaRendererState.pendingPlayQueued.value = true
} else {
DlnaRendererState.commandChannel.trySend(DlnaCommand.Play)
}
ஏற்றுக்கொள்ளப்பட்ட கட்டளைகள் ஒரு ஒற்றை Channel<DlnaCommand> வழியாக பாய்கின்றன, இதை DlnaReceiverViewModel வடிகட்டுகிறது, மூல சாக்கெட்-கையாளுதல் கோரூட்டினை UI/பிளேயர் நிலையிலிருந்து பிரிக்கிறது — HTTP கையாளுனர் ஒருபோதும் ExoPlayer-ஐ நேரடியாகத் தொடாது. பிளேபேக் பின்னர் DlnaMediaType மூலம் மூன்று முழுத்திரை காம்போசபிள்களில் ஒன்றுக்கு வழியனுப்பப்படுகிறது: DlnaReceiverAudioPlayerContent (சாய்வு பின்னணி, ஆல்பம் கலை, தேடல் பட்டை), ஒரு பட பார்வையாளர், அல்லது DlnaReceiverVideoPlayerContent (ExoPlayer).
பாதுகாப்பு: அனுமதி/மறுப்பு பட்டியல்கள் வழியாக அனுப்புநர் நம்பிக்கை
DLNA வடிவமைப்பின்படி எந்த அங்கீகாரமும் இல்லை — LAN-இல் உள்ள எந்த சாதனமும் ஒரு ரெண்டரருக்கு SetAVTransportURI அனுப்ப முடியும். ஒரு தனிப்பட்ட தொலைபேசியை அங்கீகாரமற்ற MediaRenderer ஆக மாற்றுவது, அதே Wi-Fi-இல் உள்ள எவரையும் (பகிரப்பட்ட அலுவலக நெட்வொர்க், நண்பரின் வீடு, விரோதமான விருந்தினர் நெட்வொர்க்) அதற்கு தன்னிச்சையான மீடியா URL-களைத் தள்ள அனுமதிக்கும். PlainApp இந்த இடைவெளியை ஒரு அனுப்புநர்-IP நம்பிக்கை பட்டியலுடன் மூடுகிறது, ஒவ்வொரு உள்வரும் காஸ்ட் கோரிக்கையையும் வடிகட்டுகிறது:
DlnaRendererState.rawPendingCastRequest.filterNotNull().collect { pending ->
val allowed = DlnaAllowedSendersPreference.getAsync()
val denied = DlnaDeniedSendersPreference.getAsync()
when {
DlnaAllowedSendersPreference.containsIp(allowed, pending.senderIp) -> {
// தானாக ஏற்க: உரையாடலைக் காட்டாமல் நேரடியாக கட்டளைகளை அனுப்பவும்
}
DlnaDeniedSendersPreference.containsIp(denied, pending.senderIp) -> {
// தானாக நிராகரிக்க: அமைதியாக நிராகரிக்கவும்
}
else -> {
// அறியப்படாத அனுப்புநர்: பயனர் முடிவெடுக்க UI-தெரியும் நிலைக்கு உயர்த்தவும்
DlnaRendererState.pendingCastRequest.value = pending
}
}
}
அறியப்படாத அனுப்புநரின் காஸ்ட் கோரிக்கை "இந்த தேர்வை நினைவில் கொள்" விருப்பத்துடன் கூடிய உறுதிப்படுத்தல் உரையாடலை மேற்பரப்பில் காட்டுகிறது; நினைவில் கொள்ள தேர்வு செய்வது அனுப்புநரின் IP-ஐ அனுமதி அல்லது மறுப்பு விருப்பத்தில் எழுதுகிறது, எனவே எதிர்காலத்தில் அதே முகவரியிலிருந்து வரும் கோரிக்கைகள் உரையாடலைத் தவிர்க்கும். இது முழுமையாக DlnaReceiverViewModel-இல், வயர் நெறிமுறைக்கு மேலே செயல்படுத்தப்படுகிறது — SOAP கையாளுனர் நம்பிக்கை முடிவைப் பொருட்படுத்தாமல் எப்போதும் 200 OK-ஐ வழங்குகிறது (UPnP விவரக்குறிப்பின்படி, போக்குவரத்து அழைப்பு வெற்றி பெற்றது; மீடியா உண்மையில் இயங்குகிறதா என்பது ஒரு தனி, உள்ளூர் முடிவு).
தளப் பிரிப்பு: commonMain ஒருங்கிணைப்பு, androidMain சாக்கெட்டுகள்
PlainApp-இன் பிற நெட்வொர்க் அம்சங்களைப் போலவே, மூல பைட்-நிலை சாக்கெட் I/O மட்டுமே தளம்-சார்ந்தது. DlnaServerSocket மற்றும் DlnaSsdpSocket ஆகியவை expect இடைமுகங்கள்; Android-இன் actual செயல்படுத்தல்கள் java.net.ServerSocket மற்றும் java.net.MulticastSocket-ஐ நேரடியாக மூடுகின்றன. மற்ற அனைத்தும் — போர்ட் தேர்வு, SSDP alive/byebye கேடன்ஸ், HTTP ரூட்டிங், SOAP பாகுபடுத்தல், DIDL-Lite மெட்டாடேட்டா, மற்றும் அனைத்து நான்கு DlnaCommand நிலை மாற்றங்கள் — commonMain-இல் வாழ்கின்றன மற்றும் Android சாதனம் இல்லாமல் JVM-இல் அலகு சோதிக்கக்கூடியவை.
iOS அனுப்புநர்-பக்க SOAP கட்டுப்பாட்டைப் பெறுகிறது (இது ஒரு வெற்று HTTP கிளையண்ட், செயல்படுத்த சாக்கெட்டுகள் தேவையில்லை), ஆனால் பெறுநர் வேண்டுமென்றே ஒரு செயலற்றதாக உள்ளது:
actual fun startDlnaRenderer() {}
iOS பின்னணியில் நம்பத்தகுந்த வகையில் UDP மல்டிகாஸ்ட் கேட்பியை இயக்க ஒரு வழியை வெளிப்படுத்தவில்லை, எனவே "Wireless Cast" (பெறுதல்) Android-க்கு மட்டுமே; "Cast to TV" (அனுப்புதல்) இரண்டு தளங்களிலும் வேலை செய்கிறது.
தூய-Kotlin பொறியியல் குறிப்புகள்
சில செயல்படுத்தல் விவரங்கள் குறிப்பாக Kotlin Multiplatform பகிர்வை உடைக்கும் தள சார்புகளைத் தவிர்க்க உள்ளன:
- UUID v4 உருவாக்கம்.
DlnaReceiverEngine.randomUuid()java.util.UUID.randomUUID()-க்கு பதிலாகRandom.nextBytes(16)-இலிருந்து RFC 4122 v4 UUID-ஐ கையால் உருவாக்குகிறது, எனவே சாதன அடையாள ஜெனரேட்டர் ஒவ்வொரு தளத்திலும் ஒரே மாதிரியாக இருக்கும். - சதவீத-டிகோடிங்.
DlnaSoapHandler-இன் தனிப்பட்டpercentDecode()ஒரு மீடியா URI-இல் URL-என்கோட் செய்யப்பட்டு வரும் தலைப்புகளுக்குjava.net.URLDecoder.decode()-ஐ மாற்றுகிறது. - பைட்-நிலை HTTP உடல் வாசிப்புகள்.
Content-Lengthஎன்பது ஒரு பைட் எண்ணிக்கை, எழுத்து எண்ணிக்கை அல்ல.AndroidDlnaClientConnection.readHttpRequest()மூலBufferedInputStream-க்கு எதிராகreadBodyBytes(bis, contentLength)வழியாக உடலை வாசிக்கிறது, ஒருபோதும்BufferedReader/CharArray-ஐ பயன்படுத்தாது — பல-பைட் UTF-8 (எ.கா. சீன எழுத்துக்கள், ஒவ்வொன்றும் 3 பைட்டுகள்) கொண்ட தலைப்பு இல்லையெனில் உடலை குறைவாக வாசித்து, ஏற்கனவே வந்த பைட்டுகளுக்காக காத்திருந்து நிற்கும், ASCII அல்லாத தலைப்புகளுக்குSetAVTransportURI-ஐ அமைதியாக உடைக்கும்.
வடிவமைப்பு முறைகள் மீள்பார்வை
| முறை | இடம் | காரணம் |
|---|---|---|
| பகிரப்பட்ட நெறிமுறை, பிரிக்கப்பட்ட I/O | DlnaSoap/DlnaXmlTemplates commonMain-இல், சாக்கெட்டுகள் androidMain-இல் | வயர் வடிவமைப்பு ஒருமுறை வரையறுக்கப்பட்டது, தளம் பைட்டுகளை உள்ளே/வெளியே மட்டுமே வழங்குகிறது |
| நிலுவை → விதி சரிபார்ப்பு → உயர்த்தல் | rawPendingCastRequest → pendingCastRequest | "கோரிக்கை வந்துவிட்டது" என்பதை "கோரிக்கைக்கு மனித முடிவு தேவை" என்பதிலிருந்து பிரிக்கிறது |
| கட்டளை வரிசை பிரிப்பு | Channel<DlnaCommand> | HTTP கையாளுனர் கோரூட்டின் ExoPlayer/UI நிலையை நேரடியாகத் தொடுவதில்லை |
| வரிசைப்படுத்தப்பட்ட-கட்டளை மறுஇயக்கம் | pendingPlayQueued | SetAVTransportURI-இன் ஒப்புதலுக்கு முன் வரும் Play கைவிடப்படுவதில்லை |
| வேண்டுமென்றே நிலைக் குறியீடு | respondDlnaFile → எப்போதும் 206 | விவரக்குறிப்பின் குறைந்தபட்சம் 200-ஐ அல்ல, உண்மையான டிவி ஃபார்ம்வேர் எதிர்பார்ப்பதை பொருத்துகிறது |
| குறுகிய நகல் வடிப்பான் | !xml.contains("AVTransportURIMetaData") | சில ரெண்டரர்கள் அனுப்பும் இரண்டு STOPPED அழைப்புகளை, பொதுவான நகல்நீக்க வழிமுறை இல்லாமல் வேறுபடுத்துகிறது |
| உடனடி Byebye | stop() ரத்துசெய்வதற்கு முன் ssdp:byebye அனுப்புகிறது | பயனர் அம்சத்தை அணைத்த பிறகு 30 நிமிட காலாவதியான SSDP கேச் நுழைவைத் தவிர்க்கிறது |
| தெரியாதவருக்கு திறந்த-தோல்வி, இயல்பாக மூடப்பட்ட-தோல்வி | அனுமதி/மறுப்பு விருப்பம் + உரையாடல் | முதல் முறை அனுப்புநரை தானாக நம்பவும் இல்லை, தானாக தடுக்கவும் இல்லை — ஒரு மனிதர் ஒருமுறை முடிவு செய்கிறார் |
மேலும் படிக்க
- UPnP Device Architecture — அடிப்படை SSDP/GENA/SOAP விவரக்குறிப்பு.
- WebSocket-அடிப்படையிலான குறைந்த-தாமத திரை பிரதிபலிப்பு அம்சத்தைப் பற்றி (DLNA அல்லாத வேறு காஸ்டிங் பாதை), பார்க்கவும் Screen Mirror.