DLNA (UPnP AV üzerine inşa edilmiştir), akıllı TV'lerdeki "TV'ye Yayınla" düğmelerinin arkasındaki protokoldür. Tamamen yerel ağ üzerinde çalışır, bulut hesabı veya eşleştirme adımı gerektirmez. PlainApp bu protokolün her iki yönünü de uygular: yerel bir video, şarkı veya fotoğrafı DLNA uyumlu herhangi bir akıllı TV'ye gönderebilir ve telefonun kendisini bir MediaRenderer'a dönüştürerek bir TV uzaktan kumanda uygulamasının, VLC'nin veya başka bir PlainApp'in ona yayın yapmasını sağlayabilir.
Bu makale, kablolu protokolü (SSDP + SOAP + DIDL-Lite), medyayı TV'lerin gerçekten ihtiyaç duyduğu başlıklarla yayınlayan yerel HTTP sunucusunu, oynatma durumu için GENA olay aboneliğini ve kimlik doğrulaması olmayan 1990'lar dönemi bir protokolü modern bir telefonda güvenli bir şekilde çalıştıran gönderici-IP güven modelini kapsar.
İçindekiler
- İçindekiler
- Üst Düzey Mimari
- SSDP Keşfi: Sunucu Olmadan Cihaz Bulma
- AVTransport Kontrolü: SOAP Protokolü
- DIDL-Lite Meta Verileri ve Çift Kaçış Detayı
- TV'ye Medya Sunma: Range İstekleri ve DLNA Başlıkları
- GENA Olayları: Oynatma Durumu Geri Çağrıları
- Alıcı Modu: Bir UPnP MediaRenderer Olmak
- Güvenlik: İzin/Red Listeleri ile Gönderici Güveni
- Platform Ayrımı: commonMain Orkestrasyonu, androidMain Soketleri
- Saf Kotlin Mühendislik Notları
- Tasarım Desenleri Özeti
- Daha Fazla Okuma
Üst Düzey Mimari
DLNA/UPnP AV'nin merkezi bir sunucusu veya bulut bileşeni yoktur — her şey yerel ağ üzerinden UDP çoklu yayın (keşif) ve HTTP (kontrol + medya) ile gerçekleşir. PlainApp, aynı commonMain protokol kodunu paylaşan iki bağımsız rolü uygular:
| Rol | Ne yapar | Anahtar sınıflar |
|---|---|---|
| Gönderici ("TV'ye Yayınla") | Oluşturucuları tarar, birine URL'yi getirmesini söyler, oynatmayı kontrol eder | DlnaDeviceScanner, DlnaTransportController, CastPlayer |
| Alıcı ("Kablosuz Yayın") | Kendini bir MediaRenderer olarak tanıtır, herhangi bir UPnP denetleyicisinden kontrol kabul eder | DlnaReceiverEngine, DlnaHttpRouter, DlnaSoapHandler, DlnaReceiverViewModel |
Her iki rol de features/dlna/common/ altındaki DlnaSoap (SOAP zarf oluşturucuları) ve DlnaDevice (UPnP cihaz modeli) bileşenlerini yeniden kullanır. DLNA belirtiminde hiçbir kimlik doğrulama türü yoktur — LAN üzerinde bir oluşturucunun kontrol URL'sini bilen herkes ona komut gönderebilir. Bu durum, aşağıda açıklanan hemen hemen tüm tasarım kararlarını, özellikle de alıcının güven modelini şekillendirir.
SSDP Keşfi: Sunucu Olmadan Cihaz Bulma
Keşif, 239.255.255.250:1900 adresine UDP çoklu yayın üzerinden ince bir katman olan SSDP'yi (Basit Hizmet Keşif Protokolü) kullanır. Bir dizin sunucusu yoktur — cihazlar kendilerini doğrudan duyurur ve arama sorgularını doğrudan yanıtlar.
Gönderici olarak, DlnaDeviceScanner, urn:schemas-upnp-org:service:AVTransport:1 hedefini alan bir M-SEARCH veri birimi yayınlar ve her biri oluşturucunun description.xml dosyasını gösteren bir LOCATION başlığı taşıyan tek noktaya yayın 200 OK yanıtlarını toplar. Tarayıcı, hostAddress'e göre yinelenenleri kaldırır — cihaz XML'ini kendisi ayrıştırmaz; bu iş, LOCATION'ı getiren, device.update(xml) çağıran ve yalnızca device.isAVTransport() true döndüren cihazları yüzeye çıkaran CastViewModel.searchAsync()'ye bırakılır:
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)
}
}
Alıcı olarak, DlnaReceiverEngine.runSsdpLoop() başlangıçta üç NOTIFY ssdp:alive veri birimi gönderir (kök cihaz, MediaRenderer:1 cihaz türü, AVTransport:1 hizmet türü), her 30 saniyede bir yeniden duyuru yapar (CACHE-CONTROL: max-age=1800) ve gelen M-SEARCH isteklerini tek noktaya yayın yanıtlarıyla yanıtlar. stop() çağrıldığında, 30 dakikalık önbelleğin süresinin dolmasını beklemek yerine hemen ssdp:byebye gönderir — böylece bir TV uzaktan kumanda uygulaması, "Kablosuz Yayın" kapatıldığı anda PlainApp'i listelemeyi durdurur.
Port yedekleme
Alıcının HTTP sunucusu önce 7878 portunu, ardından 7879 ve 7880 portlarını dener ve en son hangi port çalıştıysa onu tercih eder:
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
}
Eğer üç port da doluysa (nadir olsa da, başka DLNA uygulamaları çalışıyorken mümkündür), startError ayarlanır ve sessizce başarısız olmak yerine arayüzde gösterilir.
AVTransport Kontrolü: SOAP Protokolü
Bir oluşturucu bulunduğunda, oynatma UPnP AVTransport üzerinden kontrol edilir — bu, HTTP üzerinden bir SOAP hizmetidir. Her eylem, oluşturucunun kontrol URL'sine bir SOAPAction başlığı ve bir SOAP zarfına sarılmış XML gövdesi ile yapılan bir POST isteğidir.
DlnaTransportController, her isteği paylaşılan bir yardımcı işlevle oluşturur:
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>" başlığını ayarlar ve DlnaSoap.requestEnvelope(soapBody)'yi POST'lar — alıcı tarafın yanıt oluşturmak için kullandığı aynı zarf sabitleri, böylece kablolu formatın yalnızca commonMain içinde bir kez tanımlanması gerekir.
Alıcının DlnaHttpRouter.handleSoap()'ı diğer uçta bunu yansıtır: soapaction başlığını okur, # işaretinden sonraki eylem adını çıkarır ve buna göre dağıtır — SetAVTransportURI, Play, Pause, Stop, Seek, GetTransportInfo, GetPositionInfo, GetMediaInfo, GetDeviceCapabilities. RenderingControl (ses seviyesi) sabit 100 ile saplama olarak bırakılmıştır — PlainApp, UPnP üzerinden cihaz ses seviyesini açığa çıkarmaz.
DIDL-Lite Meta Verileri ve Çift Kaçış Detayı
SetAVTransportURI iki parametre taşır: CurrentURI (medya URL'si) ve CurrentURIMetaData — başlığı, medya sınıfını ve albüm kapağını tanımlayan, dış SOAP gövdesinin içine XML-kaçışlı bir dize olarak gömülü bir DIDL-Lite XML parçası:
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 iki kez kaçışa uğrar: bir kez başlık metninin kendisi için (böylece Fire & Ice adlı bir şarkı DIDL-Lite etiketlerini bozmaz) ve bir kez de tüm DIDL-Lite belgesi için (böylece kendi </> karakterleri, metin olarak gömülü olduğu dış SOAP zarfını bozmaz). Bu, iyi bilinen bir UPnP tuhaflığıdır, bir hata değil — CurrentURIMetaData, iç içe XML öğeleri olarak değil, dize içeriği olarak tanımlanmıştır.
Alıcı tarafında, DlnaSoapHandler bunu tersine çevirir: parseSoapAction, SOAP gövdesini bir kez kaçıştan çıkararak DIDL-Lite metnini elde eder, ardından extractTitleFromDidlMeta/extractMediaTypeFromDidlMeta/extractAlbumArtUriFromDidlMeta'nın her biri, bu iç dizede ikinci bir varlık kaçış çözme ve etiket çıkarma işlemi yapar:
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
}
}
Eğer <upnp:class> eksikse (bazı göndericiler bunu atlar), cleanMediaTitle() URI'nin kendi dosya uzantısına geri döner — medya türü tespiti asla sert bir şekilde başarısız olmaz, sadece UNKNOWN'a düşer ve bu da güvenli bir varsayılan olarak video oynatıcıya yönlendirilir.
TV'ye Medya Sunma: Range İstekleri ve DLNA Başlıkları
Bir SetAVTransportURI çağrısı, oluşturucuya yalnızca medyayı nereden alacağını söyler — gerçek baytlar, PlainApp'in kendi yerel HTTP sunucusu tarafından /media/{id} adresinde sunulur.
UrlHelper.getMediaHttpUrl(path), gerçek yolu (bir content:// URI'si, uzak bir URL veya düz bir dosya yolu olabilir) kısa bir kimlik altında kaydeder ve http://<cihaz-ip>:<port>/media/<id>.<uzantı> döndürür. Rota daha sonra kaynağın gerçekte ne tür olduğuna göre dallanır:
when {
path.isUrl() -> call.proxyUrl(path) // uzak URL: kaynak yanıtı akışla ilet
isContentUri(path) -> call.respondStream { sink -> streamContentUri(path, sink) }
path.isImageFast() -> call.respondFile(path) // resimler: düz statik sunum
else -> call.respondDlnaFile(path) // ses/video: DLNA bilinçli sunum
}
respondDlnaFile ilginç durumdur — birçok akıllı TV ve DLNA oluşturucu, yanıt uygun bir DLNA medya sunucusu yanıtı gibi görünmediği sürece bir akışı oynatmayı reddeder:
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) // bazı TV işletim sistemleri yalnızca 206 kabul eder
}
applicationCall.respond(LocalFileContent(file))
return true
}
Durumun her zaman 206 Partial Content olduğuna, 200 OK olmadığına dikkat edin — bazı TV üretici yazılımları, tam dosya GET'i için bile olsa, düz bir 200 yanıtını "arama yapılamaz" olarak değerlendirir ve oynatmayı reddeder. Bu tek durum kodu seçimi, birkaç gerçek cihazda "sorunsuz oynatma" ile "TV'nin sonsuza kadar dönen bir yükleyici göstermesi" arasındaki farktır.
Aynı rota, yayınlanan ses öğeleri için albüm kapağı sunmak amacıyla da kullanılır: UrlHelper.getAlbumArtHttpUrl(), bir content://media/.../albumart/<id> URI'sini aynı /media/{id} yoluna eşler, böylece "medya" ister şarkının kendisi ister kapak resmi olsun, content:// akışı ve DLNA dosya sunumu tek bir kod yolunu paylaşır.
GENA Olayları: Oynatma Durumu Geri Çağrıları
Oynatma başladıktan sonra, gönderici oluşturucunun AVTransport olay hizmetine (GENA — Genel Olay Bildirim Mimarisi) abone olur, böylece durum değişikliklerini yoklama yapmadan öğrenir:
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, standart fiil kümesinde olmayan özel HTTP yöntemleridir ve Ktor'un genel HttpMethod("SUBSCRIBE") istek oluşturucusu aracılığıyla işlenir. Oluşturucu daha sonra, taşıma durumu, konum veya süre değiştiğinde callbackUrl'ye — PlainApp'in kendi /callback/cast rotasına — NOTIFY gönderir:
if (xml.contains("TransportState val=\"STOPPED\"") && !xml.contains("AVTransportURIMetaData")) {
// oynatma listesinde sonraki öğeye geç
} else if (xml.contains("TransportState val=\"PLAYING\"")) {
CastPlayer.isPlaying.value = true
}
Çift geri çağrı koruması
Bazı oluşturucular, aynı STOPPED geçişi için hızlı bir şekilde iki NOTIFY geri çağrısı gönderir — ikincisi, ilkinde olmayan AVTransportURIMetaData'yı taşır. Oynatma listesini her ikisinde de ilerletmek, oynatma doğal olarak her durduğunda bir parçayı atlamaya neden olur. !xml.contains("AVTransportURIMetaData") kontrolü, kasıtlı, dar bir filtredir: yalnızca ilk (meta verisiz) STOPPED bildirimi otomatik ilerlemeyi tetikler. Bir startPositionUpdater() işi, yedek olarak her saniye GetPositionInfo'yu yoklar, çünkü PlainApp'in kendi SUBSCRIBE onayı, olayları diğer denetleyicilere geri göndermez — yalnızca gönderici tarafı, TV'lerden GENA geri çağrılarını tüketir.
Alıcı Modu: Bir UPnP MediaRenderer Olmak
Yönü tersine çevirin: herhangi bir DLNA denetleyicisi (bir TV uzaktan kumanda uygulaması, VLC, başka bir PlainApp) telefona medya gönderebilir. DlnaReceiverEngine, yukarıda açıklananla aynı tür HTTP + SSDP sunucusunu açar, ancak bu kez denetleyici değil, kontrol edilen MediaRenderer olarak.
DlnaHttpRouter.route(), description.xml'i sunar (DlnaXmlTemplates.deviceDescription() tarafından oluşturulur, bir AVTransport ve bir saplama RenderingControl hizmetini listeler) ve SOAP eylemlerini DlnaSoapHandler'a dağıtır. Bir SetAVTransportURI çağrısı hemen bir şey oynatmaz — bir PendingCastRequest saklar ve bekler:
if (uri.isNotEmpty()) {
DlnaRendererState.rawPendingCastRequest.value =
PendingCastRequest(senderIp, senderName, uri, title, mediaType, albumArtUri)
DlnaRendererState.pendingPlayQueued.value = false
}
Bekleyen istek çözülmeden önce gelen bir Play, oynatmayı başlatmaz — bunun yerine pendingPlayQueued = true olarak ayarlar, böylece yayın isteği kabul edildiğinde komut otomatik olarak yeniden oynatılır, sessizce kaybolmak yerine:
val hasPending = DlnaRendererState.rawPendingCastRequest.value != null ||
DlnaRendererState.pendingCastRequest.value != null
if (hasPending) {
DlnaRendererState.pendingPlayQueued.value = true
} else {
DlnaRendererState.commandChannel.trySend(DlnaCommand.Play)
}
Kabul edilen komutlar, DlnaReceiverViewModel tarafından tüketilen tek bir Channel<DlnaCommand> üzerinden akar ve ham soket işleme eşyordamını UI/oynatıcı durumundan ayırır — HTTP işleyicisi asla doğrudan ExoPlayer'a dokunmaz. Oynatma daha sonra DlnaMediaType'a göre üç tam ekran composable'dan birine yönlendirilir: DlnaReceiverAudioPlayerContent (gradyan arka plan, albüm kapağı, arama çubuğu), bir resim görüntüleyici veya DlnaReceiverVideoPlayerContent (ExoPlayer).
Güvenlik: İzin/Red Listeleri ile Gönderici Güveni
DLNA, tasarım gereği hiçbir kimlik doğrulamaya sahip değildir — LAN üzerindeki herhangi bir cihaz, bir oluşturucuya SetAVTransportURI gönderebilir. Kişisel bir telefonu kimlik doğrulamasız bir MediaRenderer'a dönüştürmek, aynı Wi-Fi üzerindeki herkesin (paylaşılan bir ofis ağı, bir arkadaşın evi, düşmanca bir misafir ağı) ona rastgele medya URL'leri göndermesine izin verir. PlainApp bu açığı, gönderici başına IP güven listesiyle kapatır ve her gelen yayın isteğini bu listeden geçirir:
DlnaRendererState.rawPendingCastRequest.filterNotNull().collect { pending ->
val allowed = DlnaAllowedSendersPreference.getAsync()
val denied = DlnaDeniedSendersPreference.getAsync()
when {
DlnaAllowedSendersPreference.containsIp(allowed, pending.senderIp) -> {
// Otomatik kabul: iletişim kutusu göstermeden doğrudan komutları gönder
}
DlnaDeniedSendersPreference.containsIp(denied, pending.senderIp) -> {
// Otomatik red: sessizce at
}
else -> {
// Bilinmeyen gönderici: kullanıcının karar vermesi için UI görünür durumuna yükselt
DlnaRendererState.pendingCastRequest.value = pending
}
}
}
Bilinmeyen bir göndericinin yayın isteği, isteğe bağlı "bu seçimi hatırla" işaretine sahip bir onay iletişim kutusu gösterir; hatırlamayı seçmek, göndericinin IP'sini izin veya red tercihine yazar, böylece aynı adresten gelen gelecek istekler iletişim kutusunu atlar. Bu tamamen DlnaReceiverViewModel içinde, kablolu protokolün üzerinde uygulanır — SOAP işleyicisinin kendisi, güven kararından bağımsız olarak her zaman 200 OK döndürür (UPnP belirtimine göre, taşıma çağrısı başarılı olmuştur; medyanın gerçekten oynatılıp oynatılmadığı ayrı, yerel bir karardır).
Platform Ayrımı: commonMain Orkestrasyonu, androidMain Soketleri
PlainApp'in diğer ağ özellikleriyle aynı deseni izleyerek, yalnızca ham bayt düzeyindeki soket G/Ç'si platforma özgüdür. DlnaServerSocket ve DlnaSsdpSocket, expect arayüzleridir; Android'in actual uygulamaları doğrudan java.net.ServerSocket ve java.net.MulticastSocket'i sarar. Geriye kalan her şey — port seçimi, SSDP alive/byebye ritmi, HTTP yönlendirme, SOAP ayrıştırma, DIDL-Lite meta verileri ve dört DlnaCommand durum geçişinin tamamı — commonMain içinde bulunur ve bir Android cihazı olmadan JVM üzerinde birim test edilebilir.
iOS, gönderici tarafı SOAP kontrolünü alır (düz bir HTTP istemcisidir, uygulanacak soket yoktur), ancak alıcı kasıtlı olarak boş bir işlemdir:
actual fun startDlnaRenderer() {}
iOS, iyi bir MediaRenderer vatandaşı olacak kadar güvenilir bir şekilde arka planda bir UDP çoklu yayın dinleyicisi çalıştırmanın bir yolunu sunmaz, bu nedenle "Kablosuz Yayın" (alma) yalnızca Android'e özgüdür; "TV'ye Yayınla" (gönderme) her iki platformda da çalışır.
Saf Kotlin Mühendislik Notları
Kotlin Multiplatform paylaşımını bozacak platform bağımlılıklarından kaçınmak için birkaç uygulama detayı mevcuttur:
- UUID v4 oluşturma.
DlnaReceiverEngine.randomUuid(),java.util.UUID.randomUUID()yerineRandom.nextBytes(16)ile elle bir RFC 4122 v4 UUID'si oluşturur, böylece cihaz kimlik oluşturucusu her platformda aynıdır. - Yüzde çözümleme.
DlnaSoapHandler'ın özelpercentDecode()işlevi, bir medya URI'sinde URL kodlanmış olarak gelen başlıklar içinjava.net.URLDecoder.decode()yerine geçer. - Bayt düzeyinde HTTP gövde okuma.
Content-Length, bir karakter sayısı değil, bir bayt sayısıdır.AndroidDlnaClientConnection.readHttpRequest(), gövdeyi hamBufferedInputStreamüzerindereadBodyBytes(bis, contentLength)aracılığıyla okur, asla birBufferedReader/CharArraykullanmaz — çok baytlı UTF-8 içeren bir başlık (örneğin, her biri 3 bayt olan Çince karakterler) aksi takdirde gövdeyi eksik okur ve zaten ulaşmış baytları bekleyerek takılıp kalır, ASCII olmayan başlıklar içinSetAVTransportURI'yi sessizce bozar. java.net.URLolmadan temel URL ayrıştırma.DlnaDevice.getBaseUrl(), birjava.net.URLoluşturmak yerine, düz dize dilimleme (substringAfter("://"),substringBefore('/')) ile birLOCATIONbaşlığından şema/ana bilgisayar/port'u çıkarır.
Tasarım Desenleri Özeti
| Desen | Nerede | Neden |
|---|---|---|
| Paylaşılan Protokol, Bölünmüş G/Ç | DlnaSoap/DlnaXmlTemplates commonMain'de, soketler androidMain'de | Kablolu format bir kez tanımlanır, platform yalnızca bayt giriş/çıkışı sağlar |
| Beklemede -> Kural Kontrolü -> Yükselt | rawPendingCastRequest -> pendingCastRequest | "Bir istek geldi" ile "bir isteğin insan kararına ihtiyacı var"ı ayırır |
| Komut Kuyruğu Ayırma | Channel<DlnaCommand> | HTTP işleyici eşyordamı asla ExoPlayer/UI durumuna doğrudan dokunmaz |
| Sıralanmış Komut Tekrar Oynatma | pendingPlayQueued | SetAVTransportURI'nin onayından önce gelen bir Play düşürülmez |
| Kasıtlı Durum Kodu | respondDlnaFile -> her zaman 206 | Yalnızca belirtim minimumu olan 200'ü değil, gerçek TV üretici yazılımının beklediğini eşleştirir |
| Dar Yinelenen Filtre | !xml.contains("AVTransportURIMetaData") | Genel bir yineleme kaldırma mekanizması olmadan, bazı oluşturucuların gönderdiği iki STOPPED geri çağrısını ayırt eder |
| Anında Byebye | stop(), iptal etmeden önce ssdp:byebye gönderir | Kullanıcı özelliği kapattıktan sonra 30 dakikalık eski bir SSDP önbellek girişinin kalmasını önler |
| Bilinmeyende Açık Başarısız, Varsayılan Olarak Kapalı Başarısız | izin/red tercihi + iletişim kutusu | İlk kez gönderen birine ne otomatik olarak güvenir ne de otomatik olarak engeller — bir insan bir kez karar verir |
Daha Fazla Okuma
- UPnP Device Architecture — altta yatan SSDP/GENA/SOAP belirtimi.
- WebSocket tabanlı düşük gecikmeli ekran yansıtma özelliği (DLNA dışı farklı bir yayın yolu) için bkz. Screen Mirror.