Inhoudsopgave
- Algehele Architectuur
- Video-encodering Pipeline (Android)
- VideoPacket-protocolontwerp
- Video-decoderingspijplijn (Web)
- WebGL2-rendering
- Pakketverliesdetectie & Foutherstel
- Afhandeling van Oriëntatiewijzigingen
- Systeem-MediaProjection-levenscyclus
- Audiopijplijn
- Prestatieoptimalisaties
- Overzicht Ontwerppatronen
Algehele Architectuur
PlainApp-schermspiegeling is een end-to-end low-latency castingsysteem: het Android-apparaat legt het scherm vast, encodeert het hardwarematig naar H.264-video en Opus-audio, en stuurt het via WebSocket met een eigen binair protocol naar de webclient; de webclient decodeert via de WebCodecs API en rendert direct naar een Canvas via WebGL2, met nul CPU-kopieën. Een transparante aanraakoverlay sluit de lus en zet aanwijzerinvoer om in gebaren op de telefoon.
Geen WebRTC, geen RTMP, geen tussenserver. De volledige pijplijn is:
Android VirtualDisplay → MediaCodec H.264 Encoder → WebSocket →
WebCodecs VideoDecoder → WebGL2 Texture → Canvas
Waarom geen WebRTC?
WebRTC is ontworpen voor real-time communicatie. De ICE/STUN/TURN-onderhandeling, congestiecontrole en jitterbuffering zijn overkill voor LAN-schermcasting. De use case van PlainApp is:
- Zelfde LAN, latentie < 5 ms, geen NAT-traversal nodig
- Streeft naar extreme lage latentie, geen jitterbuffering
- Hoge kwaliteit, bitrate kan hoog zijn (8 Mbps)
- Schermbesturing (aanraakinjectie), waarbij WebRTC's DataChannel onnodige complexiteit toevoegt
Een eigen binair protocol via WebSocket is lichter en beter beheersbaar voor het LAN-scenario.
Componentenoverzicht
| Laag | Android | Web |
|---|---|---|
| Schermopname | MediaProjection + VirtualDisplay | — |
| Video-encodering | MediaCodec H.264 hardware-encoder | — |
| Audio-encodering | MediaCodec Opus hardware-encoder | — |
| Transport | WebSocket binaire events | WebSocket-ontvanger |
| Video-decoding | — | WebCodecs VideoDecoder |
| Audio-decoding | — | WebCodecs AudioDecoder → <audio> |
| Rendering | — | WebGL2 textuur direct-render |
| Besturing | AccessibilityService gebareninjectie | Aanraakoverlay → GraphQL-mutatie |
Video en audio stromen altijd apparaat → browser via dezelfde WebSocket-verbinding; besturing stroomt in de tegenovergestelde richting via GraphQL (sendScreenMirrorControl), wat ook fungeert als nevenkanaal voor de codec-config (screenMirrorVideoCodec-query) en keyframe-verzoeken (requestScreenMirrorKeyFrame-mutatie).
Video-encodering Pipeline (Android)
Afstemming van Encoderingsparameters
Encoderingsparameters zijn specifiek afgestemd op low-latency LAN-schermcasting:
| Parameter | Waarde | Opmerkingen |
|---|---|---|
KEY_FRAME_RATE | 60 | 60 fps voor vloeiendheid |
KEY_I_FRAME_INTERVAL | 10 | IDR-interval 10s, vermindert keyframe-overhead |
KEY_BIT_RATE_MODE | VBR (impliciet, geen expliciete modus ingesteld) | Variabele bitrate, scene-adaptief |
KEY_PRIORITY | 0 | Real-time prioriteit |
KEY_LATENCY | 1 | Lage-latentiemodus |
Bitrate is ingedeeld per kwaliteitsmodus — hogere bitrates (bijv. 24 Mbps) werden getest maar veroorzaakten encoder/decoder framedrops en verhoogde end-to-end-latentie zonder zichtbare kwaliteitswinst voor scherminhoud:
| Modus | Bitrate | Opname resolutie |
|---|---|---|
| HD | 8 Mbps | 1080p korte zijde |
| Vloeiend | 4 Mbps | 1080p korte zijde |
| Laag | 2 Mbps | 720p korte zijde |
Lage-latentieconfiguratie van de Encoder
MediaCodecVideoEncoder configureert de encoder eenmalig bij aanmaak:
MediaFormat.createVideoFormat(MIME, width, height).apply {
setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
setInteger(MediaFormat.KEY_BIT_RATE, bitrateBps)
setInteger(MediaFormat.KEY_FRAME_RATE, frameRate) // 60
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameIntervalSec) // 10
setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 100_000L)
setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED)
setInteger(MediaFormat.KEY_PRIORITY, 0)
setInteger(MediaFormat.KEY_LATENCY, 1)
}
KEY_PRIORITY=0 en KEY_LATENCY=1 zijn de sleutels tot lage latentie — ze vertellen de encoder om prioriteit te geven aan real-time encoding boven compressieverhouding. De invoer is een Surface gemaakt door MediaCodec.createInputSurface() en direct gevoed aan VirtualDisplay — geen SurfaceTexture-readback, geen I420-conversie, geen CPU raakt de pixels aan.
Opname Resolutie
ScreenMirrorCaptureSize.compute() leidt de werkelijke opnamegrootte af van de fysieke schermgrootte, de korte-zijde-doelgroep van de kwaliteitsmodus (720/1080), en de door de encoder gerapporteerde maxWidth/maxHeight en breedte/hoogte-uitlijning (eenmalig opgevraagd via MediaCodecVideoEncoder.queryEncoderCaps()), zodat de encoder nooit afmetingen krijgt die hij niet kan accepteren.
Keyframe-verzoeken
De webclient kan een IDR-frame aanvragen via de GraphQL requestScreenMirrorKeyFrame-mutatie om te herstellen van pakketverlies. Android reageert via MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME:
fun requestKeyFrame() {
val b = Bundle().apply { putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 1) }
codec?.setParameters(b)
}
SPS/PPS en Keyframe-uitzending
Nadat de encoder is gestart, levert INFO_OUTPUT_FORMAT_CHANGED csd-0/csd-1 (SPS/PPS), die ScreenMirrorPipeline samenvoegt tot een enkele Annex-B-config-blob en cachet (cachedConfig). De eerste IDR die volgt wordt ook gecachet (cachedKeyFrame), zodat een pas verbonden webclient beide kan ophalen via de screenMirrorVideoCodec GraphQL-query zonder te wachten op het volgende keyframe-interval. Wanneer de config zojuist is gewijzigd (oriëntatie- of kwaliteitswissel), stuurt Android de nieuwe IDR niet als een normaal videopakket — het bundelt SPS/PPS + IDR in een enkele screen_mirror_video_codec WebSocket-gebeurtenis, zodat de webclient decoder-herconfiguratie en eersteframe-decoding in een keer voltooit in plaats van een verouderde decoder te laten racen tegen een nieuwe bitstream.
Sommige OEM-encoders (Qualcomm/Xiaomi) bundelen SPS+PPS+IDR in een enkele uitvoerbuffer die zowel BUFFER_FLAG_CODEC_CONFIG als BUFFER_FLAG_SYNC_FRAME draagt. De drain-lus slaat alleen buffers over die puur config zijn (isConfig && !isKey) — het overslaan van een config-gemarkeerde buffer die ook het sync-frame bevat, zou de IDR stilletjes laten vallen en de decoder alleen met P-frames achterlaten, wat mozaïekuitvoer produceert.
VideoPacket-protocolontwerp
Zowel video- als audioframes zijn verpakt in het uniforme VideoPacket binaire protocol voor WebSocket-transport.
Protocolformaat
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| MAGIC | FLAGS | FRAME_ID (4 bytes, big-endian) | TIMESTAMP (8 bytes, BE) | DATA |
| 0x56 | | byte2 | byte3 | byte4 | byte5 | byte6 | byte7 | ... | byte13 | payload... |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+
\- 1B -/ \- 1B -/ \-------------------- 4B ----------------------/ \----------------------- 8B ------------------------/ \- var -/
| Veld | Grootte | Beschrijving |
|---|---|---|
MAGIC | 1 byte | Vast 0x56 ('V'), voor validatie |
FLAGS | 1 byte | 0x01=keyframe, 0x02=config, 0x04=audio |
FRAME_ID | 4 bytes | Monotoon oplopend framenummer, uint32 big-endian |
TIMESTAMP | 8 bytes | Encoder PTS in microseconden, big-endian |
DATA | variabel | H.264 NAL-unit of Opus-data |
Zowel Android's VideoPacket.encode() (in commonMain, dus het draadformaat wordt gedekt door JVM-unittests zonder enige Android-afhankelijkheid) als de web's parseVideoPacket() implementeren dit formaat onafhankelijk — er is geen gedeelde serialisatiebibliotheek, alleen een specificatie waaraan beide kanten zich houden.
Ontwerpnotities
FRAME_IDunsigned-parsing:((buf[2] << 24) | (buf[3] << 16) | (buf[4] << 8) | buf[5]) >>> 0— moet>>> 0gebruiken om unsigned te garanderen, anders wordtframeId > 2^31als negatief geparseerd, wat valse verliesdetectie veroorzaakt.FRAME_IDwordt nooit gereset: Wanneer de encoder opnieuw wordt opgebouwd vanwege een oriëntatiewijziging, blijftframeIdoplopen (het leeft inScreenMirrorPipeline, niet in de encoder). Hierdoor kan de webkant frameverlies tijdens rotatie detecteren via frameId-gaten.TIMESTAMPgebruikt encoder PTS: Geen afhankelijkheid van de klok van de webclient, waardoor klokdrift A/V-desynchronisatie voorkomt.- Zero-copy-parsing: de webparser snijdt de payload met
Uint8Array.subarray()— een weergave in de originele WebSocketArrayBuffer, geen kopie.
Video-decoderingspijplijn (Web)
WebCodecs VideoDecoder
De webclient gebruikt de VideoDecoder van de WebCodecs API voor hardware-decoding. Vergeleken met MediaSource Extensions of WebRTC biedt WebCodecs fijnmazige controle over het decoderingsproces — geen jitterbuffer, geen containerlaag, en gedecodeerde VideoFrame-objecten kunnen direct als WebGL-texturen worden geüpload.
const decoder = new VideoDecoder({
output: (frame) => this.renderFrame(frame),
error: (e) => {
this.waitingForIdr = true
this.onRequestKeyFrame?.()
this.onError?.(e)
},
})
decoder.configure({
codec, // bijv. 'avc1.42c01e', uitgelezen uit de SPS NAL
avc: { format: 'annexb' },
optimizeForLatency: true,
hardwareAcceleration: 'prefer-hardware',
})
Belangrijke configuraties:
optimizeForLatency: true— vertelt de decoder om prioriteit te geven aan lage latentie, geen framebufferinghardwareAcceleration: 'prefer-hardware'— geef de voorkeur aan GPU-decodingavc: { format: 'annexb' }— gebruik Annex-B-formaat met inline SPS/PPS voor elke IDR- de codec-string zelf is niet hardgecodeerd —
extractAvc1CodecString()leest profiel/compat/niveau-bytes rechtstreeks uit de eerste SPS NAL in de config-blob
Groenschermprobleem en Opstartvolgorde
De encoder produceert zijn eerste IDR-frame voordat VirtualDisplay echte scherminhoud heeft weergegeven — het is een leeg (groen) frame. Als de webkant dit frame decodeert, ziet de gebruiker een groene flits totdat de scherminhoud verandert en een nieuw frame triggert.
Oplossing: Bij het opstarten haalt de webkant de gecachete config op via de screenMirrorVideoCodec GraphQL-query maar decodeert het gebundelde keyframe niet. In plaats daarvan roept het video.requestIdr() aan om waitingForIdr = true te zetten (alle P-frames laten vallen totdat een IDR arriveert), en roept vervolgens requestKeyFrame() aan om een verse IDR aan te vragen via dezelfde mutatie die voor verliesherstel wordt gebruikt. Tegen de tijd dat de nieuwe IDR arriveert, heeft VirtualDisplay echte scherminhoud.
video.requestIdr() // P-frames laten vallen, wachten op IDR
await requestKeyFrame() // verse IDR aanvragen via GraphQL-mutatie
De onFirstFrameRendered-callback is gebonden aan renderFrame() in plaats van handleVideo(), zodat de UI pas wordt bijgewerkt nadat een echt frame is gerenderd — niet alleen ontvangen.
WebGL2-rendering
Zero-Copy GPU Direct Renderen
Gedecodeerde VideoFrame-objecten worden direct als WebGL2-texturen geüpload, nooit via de CPU:
VideoDecoder → VideoFrame → gl.texImage2D(VideoFrame) → Canvas
gl.texImage2D accepteert VideoFrame als pixelbron. De browser handelt YUV→RGB-conversie en GPU-upload intern af — geen ImageData CPU-kopie. MirrorGLRenderer valt terug op Canvas 2D drawImage() als getContext('webgl2', ...) faalt, zodat oudere browsers nog steeds een (iets hogere latentie) beeld krijgen.
desynchronized Context
const gl = canvas.getContext('webgl2', {
alpha: false,
desynchronized: true, // compositor omzeilen, direct naar scherm schrijven
preserveDrawingBuffer: true, // buffer behouden voor screenshots
powerPreference: 'high-performance',
antialias: false,
depth: false,
stencil: false,
premultipliedAlpha: false,
})
desynchronized: true omzeilt de browsercompositor, schrijft direct naar het scherm, bespaart ~1 frame weergavelatentie (~16ms @ 60fps).
preserveDrawingBuffer: true behoudt de tekenbuffer zodat canvas.toDataURL()-screenshots de inhoud kunnen lezen. Met de standaard false wordt de buffer gewist na compositing, wat zwarte screenshots oplevert.
De shader zelf is bewust minimaal — een fullscreen-driehoek vertex-shader en een eenregelige fragment-shader die de textuur sampled — omdat het enige werk dat per frame nodig is "zet deze textuur op het scherm" is.
Canvas Automatisch Aanpassen
De Canvas-backing-store-grootte wordt ingesteld op VideoFrame.displayWidth/Height wanneer deze verandert. De CSS-grootte wordt vervolgens aangepast aan de wrapper-container door fitCanvasToWrapper() met behoud van aspectratio (letterboxing of pillarboxing indien nodig). Een ResizeObserver op het bovenliggende element van de canvas voert deze aanpassing opnieuw uit wanneer de container van grootte verandert, zodat de video nooit uitrekt.
Pakketverliesdetectie & Foutherstel
FrameId-Gatdetectie
Elk videoframe draagt een monotoon oplopende frameId. De decoder houdt lastFrameId bij; als de frameId van een nieuw frame > lastFrameId + 1 is, zijn er frames verloren gegaan:
if (!this.waitingForIdr && this.lastFrameId > 0
&& packet.frameId > this.lastFrameId + 1) {
if (!packet.isKeyFrame) {
// Verlies: volgende P-frames laten vallen, nieuwe IDR aanvragen
this.waitingForIdr = true
this.onRequestKeyFrame?.()
this.lastFrameId = packet.frameId
return
}
}
waitingForIdr-toestandsmachine
waitingForIdr is een eenvoudige twee-toestandsmachine:
| Toestand | Gedrag |
|---|---|
NORMAL | Decodeer alle frames normaal |
WAITING_FOR_IDR | Laat alle P-frames vallen, decodeer alleen IDR-frames; reset naar NORMAL wanneer IDR arriveert |
Scenario's die de overgang naar WAITING_FOR_IDR triggeren:
- Bij opstarten: sla verouderd GraphQL-keyframe over, wacht op echte IDR
- Bij pakketverlies: laat ondecodeerbare P-frames vallen, wacht op IDR-herstel
- Bij decoderfout: reset decoder, wacht op IDR
- Bij configuratiewijziging: laat resterende P-frames vallen na oriëntatie-/kwaliteitswijziging
Decoderfoutherstel
Wanneer VideoDecoder.onerror wordt geactiveerd, wordt decoderNeedsReset = true gezet in de pijplijnlaag (screen-mirror-pipeline.ts). Bij het volgende IDR-frame wordt de decoder opnieuw geconfigureerd met de gecachete SPS/PPS in plaats van opnieuw naar GraphQL te gaan:
if (decoderNeedsReset) {
if (!packet.isKeyFrame || !cachedConfig) return
video.configure(cachedConfig)
decoderNeedsReset = false
}
Tegenwerkingsdruk en Tijdstempeldeduplicatie
Als decoder.decodeQueueSize > 5, worden inkomende P-frames laten vallen in plaats van in de wachtrij geplaatst — de drempel van 5 (in plaats van 2) tolereert opstartlatentie van de hardware-decoder zonder onnodige haperingen te veroorzaken. Afzonderlijk wordt na het renderen lastRenderedPts geregistreerd; een frame waarvan de tijdstempel ouder is (buiten-volgorde-aankomst) wordt laten vallen tenzij het een keyframe is:
if (packet.timestamp < this.lastRenderedPts && !packet.isKeyFrame) {
return
}
Afhandeling van Oriëntatiewijzigingen
Encoder Opnieuw Opbouwen
Een OrientationEventListener in ScreenMirrorService vergelijkt de rotation van het display met de gecachete isPortrait-vlag bij elke sensor-callback; alleen een echte portret/landschap-omslag roept pipeline.onOrientationChanged() aan en invalideert de toegankelijkheidsschermgrootte-cache die wordt gebruikt voor het schalen van aanraakcoördinaten.
rebuildEncoderAndResize():
- Maak een nieuwe encoder op de nieuwe afmetingen (bijv. landschap 1920×1080)
- Schakel
VirtualDisplay.surfaceover naar de invoer-Surfacevan de nieuwe encoder - Stop de oude encoder
VirtualDisplay.resize()naar de nieuwe afmetingen
Surface-omschakeling gebeurt vóór resize — zodat de nieuwe encoder eerst frames ontvangt, en de oude encoder wordt gestopt voordat deze frames met verkeerde afmetingen kan ontvangen. Als virtualDisplay?.surface = ... een fout geeft, wordt de herbouw afgebroken en blijft de oude encoder draaien in plaats van de pijplijn zonder encoder achter te laten.
Configuratiewijzigingsmelding
Wanneer de nieuwe encoder voor het eerst SPS/PPS uitvoert, wordt pendingConfigBroadcast op de pijplijn gezet. Wanneer de eerste IDR van de nieuwe encoder arriveert, wordt deze samen met die config gebundeld in een enkele screen_mirror_video_codec-gebeurtenis in plaats van als een gewoon videopakket te worden verzonden.
De webclient handelt dit vervolgens af in handleConfig():
- Configureert de decoder opnieuw met de nieuwe SPS/PPS
- Decodeert het gebundelde IDR-frame onmiddellijk
- Roept
video.requestIdr()aan om eventuele resterende P-frames van de oude encoder die nog onderweg zijn te laten vallen - Roept
requestKeyFrame()aan om een schone, verse IDR aan te vragen
Stappen 3-4 zijn een vangnet — zelfs als de eerste IDR van de nieuwe encoder onjuiste afmetingen heeft (tijdens het asynchrone resize-venster), herstelt de webclient snel naar de juiste afmetingen. handleConfig() breekt ook vroegtijdig af als de inkomende config byte-identiek is aan de gecachete, omdat het opnieuw configureren van de decoder met ongewijzigde bytes een no-op is die toch een IDR kost om van te herstellen.
Systeem-MediaProjection-levenscyclus
Het Probleem
Gebruikers kunnen de systeem-level schermcast (MediaProjection) sluiten via de Android-systeemmeldingenbalk, in plaats van via de app-interface. In dit geval weet ScreenMirrorService niet dat casten is gestopt — running blijft true, de webclient vraagt screenMirrorState op en krijgt true, maar er arriveren geen videoframes en de pagina blijft hangen op laden.
MediaProjection.Callback
MediaProjection biedt een Callback.onStop()-callback die wordt geactiveerd wanneer het systeem stopt met casten. ScreenMirrorPipeline.startEncoders() registreert deze callback en roept ScreenMirrorService.instance?.stop() aan in onStop():
projection.registerCallback(object : MediaProjection.Callback() {
override fun onStop() {
ScreenMirrorService.instance?.stop()
}
}, null)
Service.stop() Verantwoordelijkheden
stop() is het expliciete stoppunt, verantwoordelijk voor het op de hoogte stellen van de webclient en het stoppen van de service:
fun stop() {
if (!running) return // recursie voorkomen
running = false
sendEvent(WebSocketEvent(EventType.SCREEN_MIRRORING, """{"running":false}"""))
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
De if (!running) return-beveiliging voorkomt recursie: onStop() → stop() → stopSelf() → onDestroy() → pipeline.stop() → projection.stop() → onStop() → stop() (op dit punt is running=false, keert direct terug).
Web-Kant Afhandeling
Wanneer de webclient de {"running":false}-gebeurtenis ontvangt, reset deze naar de inactieve toestand en toont de startknop:
const onScreenMirroring = (data: any) => {
if (data?.running === false) {
cleanupFn()
fullReset()
return
}
// running=true → verbinden met stream
}
Audiopijplijn
Android Opus-encodering
MediaCodecAudioEncoder gebruikt AudioPlaybackCaptureConfiguration (gebouwd van dezelfde MediaProjection) om systeemaudio vast te leggen via AudioRecord, en voert ruwe PCM in een MediaCodec Opus-encoder. Dit vereist Android 10+ en de RECORD_AUDIO-machtiging — op oudere apparaten of zonder de machtiging logt start() een waarschuwing en slaat audio over (video blijft werken). Gecodeerde Opus-pakketten worden verpakt in hetzelfde VideoPacket-protocol (met FLAG_AUDIO ingesteld) en delen het videopakket's SCREEN_MIRROR_AUDIO WebSocket-kanaal.
Web Opus-decoding
ScreenMirrorAudioPipeline gebruikt WebCodecs AudioDecoder om Opus-data te decoderen, met AudioData-uitvoer die naar een <audio>-element wordt gestuurd. De audiotijdstempel (timestamp) wordt gebruikt voor A/V-synchronisatie — dezelfde tijdbasis (encoder PTS, in microseconden) als videoframes, dus er is geen aparte klokonderhandeling nodig tussen de twee stromen.
Prestatieoptimalisaties
Zero-Copy-paden
| Pad | Methode |
|---|---|
| VirtualDisplay → encoder Surface | GPU direct, Surface-doorvoer |
| VideoDecoder → VideoFrame → WebGL-textuur | gl.texImage2D(VideoFrame), GPU direct |
| WebSocket-ontvangst → VideoPacket-parsing | Uint8Array.subarray() is een weergave, geen kopie |
avccToAnnexB Optimalisatie
Sommige Android-encoders voeren AVCC-formaat uit (4-byte lengtevoorvoegsel), wat moet worden geconverteerd naar Annex-B-formaat (00 00 00 01-startcode) voor WebCodecs-decoding.
De vroege implementatie gebruikte ArrayList<Byte> met per-byte boxing — een 50KB IDR-frame produceerde 50.000 java.lang.Byte-boxing-operaties, wat enorme GC-druk veroorzaakte. De optimalisatie gebruikt een tweepas-scan + copyInto (wat op de JVM wordt toegewezen aan de System.arraycopy-intrinsic):
// Eerste pas: uitvoergrootte berekenen
var outSize = 0
// Tweede pas: bulk-kopiëren
val out = ByteArray(outSize)
avcc.copyInto(out, writeOff + 4, off + 4, off + 4 + len)
P-Frame Drop Strategie
De decoder kan traag zijn tijdens initialisatie. Als de P-frame-wachtrij te lang wordt, hoopt latentie zich op. De drempel voor de decodeerwachtrijgrootte is ingesteld op > 5 (in plaats van > 2) om overmatig frameverlies tijdens hardware-decoderinitialisatie te voorkomen.
IDR-verzoek Deduplicatie
De waitingForIdr-beveiliging zorgt ervoor dat er slechts één IDR-verzoek per verliesgebeurtenis wordt gedaan, waardoor dubbele verzoeken worden voorkomen terwijl wordt gewacht op een IDR.
Overzicht Ontwerppatronen
| Patroon | Waar | Waarom |
|---|---|---|
| Toestandsmachine | waitingForIdr-vlag | Expliciete P-frame-drop/herstel-toestandsovergangen |
| Recursiebeveiliging | if (!running) return in stop() | Voorkomt onStop → stop → onDestroy → pipeline.stop → projection.stop → onStop-recursie |
| Zero-Copy-pijplijn | VideoFrame → gl.texImage2D | GPU directe textuur-upload, geen CPU-kopie |
| Tweepas-scan | avccToAnnexB | Vooraf grootte berekenen, enkele allocatie + bulk-kopiëren, elimineert boxing |
| Gebundelde Gebeurtenis | SPS/PPS + IDR in een gebeurtenis | Configuratiewijziging voltooit herconfiguratie + eersteframe-decoding in een gebeurtenis |
| Callback-scheiding | onFirstFrameRendered vs onDisconnected vs onScreenMirrorOff | Duidelijk onderscheid tussen eersteframe-render, transportfout en telefoon-kant stop |
| Vangnet | requestIdr() + requestKeyFrame() | Resterende frames laten vallen + schone IDR aanvragen na configuratiewijziging |
| PTS-deduplicatie | timestamp < lastRenderedPts | Buiten-volgorde frames laten vallen |
| FrameId-gat | frameId > lastFrameId + 1 | ACK-vrije pakketverliesdetectie |
| desynchronized Context | WebGL2 desynchronized: true | Compositor omzeilen, 1 frame latentie besparen |
| Expliciet Snel Falen | sendScreenMirrorControl gooit GraphQLError | Meldt "toegankelijkheid uitgeschakeld" in plaats van invoer stilletjes te negeren |
Verder Lezen
- WebCodecs API — MDN-documentatie over de
VideoDecoder/AudioDecoder-interfaces.