DLNA (UPnP AV पर आधारित) स्मार्ट TV पर "Cast to TV" बटन के पीछे का प्रोटोकॉल है। यह पूरी तरह से लोकल नेटवर्क पर चलता है, जिसमें न क्लाउड अकाउंट की ज़रूरत है और न ही किसी पेयरिंग स्टेप की। PlainApp इसे दोनों दिशाओं में लागू करता है: यह किसी भी DLNA-संगत स्मार्ट TV पर लोकल वीडियो, गाना या फ़ोटो पुश कर सकता है, और फ़ोन को ही एक MediaRenderer में बदल सकता है ताकि TV रिमोट ऐप, VLC या कोई दूसरा PlainApp उस पर कास्ट कर सके।
यह लेख वायर प्रोटोकॉल (SSDP + SOAP + DIDL-Lite), लोकल HTTP सर्वर जो TV के लिए ज़रूरी हेडर्स के साथ मीडिया स्ट्रीम करता है, प्लेबैक स्टेट के लिए GENA इवेंट सब्सक्रिप्शन, और सेंडर-IP ट्रस्ट मॉडल को कवर करता है जो इस बिना-प्रमाणीकरण वाले 1990 के दशक के प्रोटोकॉल को आधुनिक फ़ोन पर सुरक्षित रूप से चलाने योग्य बनाता है।
विषय सूची
- विषय सूची
- उच्च-स्तरीय आर्किटेक्चर
- SSDP डिस्कवरी: बिना सर्वर के डिवाइस ढूँढना
- AVTransport कंट्रोल: SOAP प्रोटोकॉल
- DIDL-Lite मेटाडेटा और डबल-एस्केपिंग क्वर्क
- TV को मीडिया सर्व करना: Range रिक्वेस्ट और DLNA हेडर्स
- GENA इवेंट्स: प्लेबैक स्टेट कॉलबैक
- रिसीवर मोड: UPnP MediaRenderer बनना
- सुरक्षा: अनुमति/अस्वीकार सूचियों के ज़रिए सेंडर ट्रस्ट
- प्लेटफ़ॉर्म विभाजन: commonMain ऑर्केस्ट्रेशन, androidMain सॉकेट्स
- शुद्ध Kotlin इंजीनियरिंग नोट्स
- डिज़ाइन पैटर्न्स का पुनरावलोकन
- आगे पढ़ने के लिए
उच्च-स्तरीय आर्किटेक्चर
DLNA/UPnP AV में कोई केंद्रीय सर्वर या क्लाउड घटक नहीं है — सब कुछ लोकल-नेटवर्क UDP मल्टीकास्ट (डिस्कवरी) और HTTP (कंट्रोल + मीडिया) पर होता है। PlainApp दो स्वतंत्र भूमिकाएँ लागू करता है जो एक ही commonMain प्रोटोकॉल कोड साझा करती हैं:
| भूमिका | यह क्या करती है | मुख्य क्लासेज़ |
|---|---|---|
| सेंडर ("TV पर कास्ट करें") | रेंडरर्स को स्कैन करता है, एक को URL फ़ेच करने का निर्देश देता है, प्लेबैक को कंट्रोल करता है | DlnaDeviceScanner, DlnaTransportController, CastPlayer |
| रिसीवर ("वायरलेस कास्ट") | खुद को 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 डेटाग्राम प्रसारित करता है और यूनिकास्ट 200 OK उत्तर एकत्र करता है, जिनमें से प्रत्येक में रेंडरर के description.xml की ओर इशारा करता एक LOCATION हेडर होता है। स्कैनर hostAddress द्वारा डुप्लीकेट हटाता है — यह डिवाइस XML को स्वयं पार्स नहीं करता; यह काम CastViewModel.searchAsync() पर छोड़ दिया जाता है, जो LOCATION फ़ेच करता है, device.update(xml) कॉल करता है, और केवल उन डिवाइसों को दिखाता है जहाँ device.isAVTransport() true रिटर्न करता है:
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 भेजता है — ताकि TV रिमोट ऐप "वायरलेस कास्ट" बंद होते ही 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-ओवर-HTTP सेवा है। हर क्रिया रेंडरर के कंट्रोल URL पर एक POST है जिसमें SOAPAction हेडर और SOAP एन्वेलप में लिपटा एक XML बॉडी होता है।
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 DIDL-Lite टेक्स्ट प्राप्त करने के लिए SOAP बॉडी को एक बार अन-एस्केप करता है, फिर 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 पर डिग्रेड हो जाता है जो एक सुरक्षित डिफ़ॉल्ट के रूप में वीडियो प्लेयर पर रूट होता है।
TV को मीडिया सर्व करना: Range रिक्वेस्ट और DLNA हेडर्स
SetAVTransportURI कॉल केवल रेंडरर को बताता है कि मीडिया कहाँ से फ़ेच करना है — वास्तविक बाइट्स PlainApp के अपने लोकल HTTP सर्वर द्वारा /media/{id} पर सर्व की जाती हैं।
UrlHelper.getMediaHttpUrl(path) वास्तविक पथ (जो content:// URI, एक रिमोट URL, या एक सादा फ़ाइल पथ हो सकता है) को एक छोटी id के अंतर्गत रजिस्टर करता है और http://<device-ip>:<port>/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 दिलचस्प मामला है — कई स्मार्ट TV और 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 नहीं — कुछ TV फ़र्मवेयर एक सादे 200 रिस्पॉन्स को "सीक करने योग्य नहीं" मानते हैं और इसे चलाने से मना कर देते हैं, भले ही वह पूर्ण-फ़ाइल GET ही क्यों न हो। यह एक स्टेटस-कोड का चुनाव कई वास्तविक डिवाइसों पर "ठीक चलता है" और "TV हमेशा स्पिनर दिखाता है" के बीच का अंतर है।
वही रूट कास्ट ऑडियो आइटम्स के लिए एल्बम आर्ट भी सर्व करता है: 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 स्वीकारोक्ति अन्य कंट्रोलर्स को इवेंट वापस नहीं भेजता है — केवल सेंडर पक्ष TV से GENA कॉलबैक का उपभोग करता है।
रिसीवर मोड: UPnP MediaRenderer बनना
दिशा पलटें: कोई भी DLNA कंट्रोलर (TV रिमोट ऐप, 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 क्लाइंट है, इसमें लागू करने के लिए कोई सॉकेट नहीं है), लेकिन रिसीवर जानबूझकर एक no-op है:
actual fun startDlnaRenderer() {}
iOS एक अच्छा MediaRenderer नागरिक होने के लिए पर्याप्त विश्वसनीय रूप से बैकग्राउंड UDP मल्टीकास्ट लिसनर चलाने का कोई तरीका एक्सपोज़ नहीं करता है, इसलिए "वायरलेस कास्ट" (प्राप्त करना) केवल Android के लिए है; "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को तोड़ देगा। java.net.URLके बिना बेस URL पार्सिंग।DlnaDevice.getBaseUrl()java.net.URLबनाने के बजाय सादे स्ट्रिंग स्लाइसिंग (substringAfter("://"),substringBefore('/')) के साथLOCATIONहेडर से स्कीम/होस्ट/पोर्ट निकालता है।
डिज़ाइन पैटर्न्स का पुनरावलोकन
| पैटर्न | स्थान | कारण |
|---|---|---|
| साझा प्रोटोकॉल, विभाजित I/O | DlnaSoap/DlnaXmlTemplates commonMain में, सॉकेट्स androidMain में | वायर फ़ॉर्मेट एक बार परिभाषित, प्लेटफ़ॉर्म केवल बाइट्स इन/आउट प्रदान करता है |
| लंबित → नियम जाँच → प्रमोट | rawPendingCastRequest → pendingCastRequest | "एक अनुरोध आ गया" को "एक अनुरोध को मानवीय निर्णय चाहिए" से अलग करता है |
| कमांड क्यू डिकपलिंग | Channel<DlnaCommand> | HTTP हैंडलर कोरूटीन कभी सीधे ExoPlayer/UI स्टेट को नहीं छूता |
| क्यू-बद्ध कमांड रीप्ले | pendingPlayQueued | एक Play जो SetAVTransportURI की स्वीकृति से आगे निकल जाता है, खोया नहीं जाता |
| जानबूझकर स्टेटस कोड | respondDlnaFile → हमेशा 206 | वास्तविक TV फ़र्मवेयर अपेक्षा से मेल खाता है, न कि केवल स्पेक-न्यूनतम 200 |
| संकीर्ण डुप्लीकेट फ़िल्टर | !xml.contains("AVTransportURIMetaData") | एक सामान्य डिडप तंत्र के बिना, कुछ रेंडरर्स द्वारा भेजे गए दो STOPPED कॉलबैक के बीच अंतर करता है |
| तत्काल Byebye | stop() रद्द करने से पहले ssdp:byebye भेजता है | उपयोगकर्ता द्वारा सुविधा बंद करने के बाद 30 मिनट की पुरानी SSDP कैैश एंट्री से बचाता है |
| अज्ञात पर खुला-विफल, डिफ़ॉल्ट रूप से बंद-विफल | अनुमति/अस्वीकार प्राथमिकता + डायलॉग | पहली बार के सेंडर को न तो स्वचालित रूप से भरोसा करता है और न ही ब्लॉक करता है — एक मानव एक बार निर्णय लेता है |
आगे पढ़ने के लिए
- UPnP Device Architecture — अंतर्निहित SSDP/GENA/SOAP स्पेसिफिकेशन।
- WebSocket-आधारित कम-लेटेंसी स्क्रीन मिररिंग सुविधा (एक अलग, गैर-DLNA कास्टिंग पथ) के लिए, देखें Screen Mirror।