Terug naar blog
Architecture15 min read

Schermspiegeling: Low-Latency Casting-architectuur

Dit artikel behandelt de end-to-end-ontwerp van PlainApp's schermspiegelingssysteem: hoe Android met MediaCodec H.264/Opus hardwarematig encodeert, hoe frames via WebSocket met een eigen binair protocol worden getransporteerd, hoe de webkant via WebCodecs decodeert en via WebGL2 met zero CPU-kopieën rendert, hoe pakketverlies, oriëntatiewijzigingen en aanraakbediening op afstand worden afgehandeld, en hoe de systeem-MediaProjection-levenscyclus gesynchroniseerd blijft.

Inhoudsopgave

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

Diagram 1
1

LaagAndroidWeb
SchermopnameMediaProjection + VirtualDisplay—
Video-encoderingMediaCodec H.264 hardware-encoder—
Audio-encoderingMediaCodec Opus hardware-encoder—
TransportWebSocket binaire eventsWebSocket-ontvanger
Video-decoding—WebCodecs VideoDecoder
Audio-decoding—WebCodecs AudioDecoder → <audio>
Rendering—WebGL2 textuur direct-render
BesturingAccessibilityService gebareninjectieAanraakoverlay → 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:

ParameterWaardeOpmerkingen
KEY_FRAME_RATE6060 fps voor vloeiendheid
KEY_I_FRAME_INTERVAL10IDR-interval 10s, vermindert keyframe-overhead
KEY_BIT_RATE_MODEVBR (impliciet, geen expliciete modus ingesteld)Variabele bitrate, scene-adaptief
KEY_PRIORITY0Real-time prioriteit
KEY_LATENCY1Lage-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:

ModusBitrateOpname resolutie
HD8 Mbps1080p korte zijde
Vloeiend4 Mbps1080p korte zijde
Laag2 Mbps720p 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 -/
VeldGrootteBeschrijving
MAGIC1 byteVast 0x56 ('V'), voor validatie
FLAGS1 byte0x01=keyframe, 0x02=config, 0x04=audio
FRAME_ID4 bytesMonotoon oplopend framenummer, uint32 big-endian
TIMESTAMP8 bytesEncoder PTS in microseconden, big-endian
DATAvariabelH.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.

Diagram 2
2

Ontwerpnotities

  • FRAME_ID unsigned-parsing: ((buf[2] << 24) | (buf[3] << 16) | (buf[4] << 8) | buf[5]) >>> 0 — moet >>> 0 gebruiken om unsigned te garanderen, anders wordt frameId > 2^31 als negatief geparseerd, wat valse verliesdetectie veroorzaakt.
  • FRAME_ID wordt nooit gereset: Wanneer de encoder opnieuw wordt opgebouwd vanwege een oriëntatiewijziging, blijft frameId oplopen (het leeft in ScreenMirrorPipeline, niet in de encoder). Hierdoor kan de webkant frameverlies tijdens rotatie detecteren via frameId-gaten.
  • TIMESTAMP gebruikt 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 WebSocket ArrayBuffer, 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 framebuffering
  • hardwareAcceleration: 'prefer-hardware' — geef de voorkeur aan GPU-decoding
  • avc: { 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:

Diagram 3
3

ToestandGedrag
NORMALDecodeer alle frames normaal
WAITING_FOR_IDRLaat alle P-frames vallen, decodeer alleen IDR-frames; reset naar NORMAL wanneer IDR arriveert

Scenario's die de overgang naar WAITING_FOR_IDR triggeren:

  1. Bij opstarten: sla verouderd GraphQL-keyframe over, wacht op echte IDR
  2. Bij pakketverlies: laat ondecodeerbare P-frames vallen, wacht op IDR-herstel
  3. Bij decoderfout: reset decoder, wacht op IDR
  4. 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.

Diagram 4
4

rebuildEncoderAndResize():

  1. Maak een nieuwe encoder op de nieuwe afmetingen (bijv. landschap 1920×1080)
  2. Schakel VirtualDisplay.surface over naar de invoer-Surface van de nieuwe encoder
  3. Stop de oude encoder
  4. 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():

  1. Configureert de decoder opnieuw met de nieuwe SPS/PPS
  2. Decodeert het gebundelde IDR-frame onmiddellijk
  3. Roept video.requestIdr() aan om eventuele resterende P-frames van de oude encoder die nog onderweg zijn te laten vallen
  4. 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)

Diagram 5
5

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

PadMethode
VirtualDisplay → encoder SurfaceGPU direct, Surface-doorvoer
VideoDecoder → VideoFrame → WebGL-textuurgl.texImage2D(VideoFrame), GPU direct
WebSocket-ontvangst → VideoPacket-parsingUint8Array.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

PatroonWaarWaarom
ToestandsmachinewaitingForIdr-vlagExpliciete P-frame-drop/herstel-toestandsovergangen
Recursiebeveiligingif (!running) return in stop()Voorkomt onStop → stop → onDestroy → pipeline.stop → projection.stop → onStop-recursie
Zero-Copy-pijplijnVideoFrame → gl.texImage2DGPU directe textuur-upload, geen CPU-kopie
Tweepas-scanavccToAnnexBVooraf grootte berekenen, enkele allocatie + bulk-kopiëren, elimineert boxing
Gebundelde GebeurtenisSPS/PPS + IDR in een gebeurtenisConfiguratiewijziging voltooit herconfiguratie + eersteframe-decoding in een gebeurtenis
Callback-scheidingonFirstFrameRendered vs onDisconnected vs onScreenMirrorOffDuidelijk onderscheid tussen eersteframe-render, transportfout en telefoon-kant stop
VangnetrequestIdr() + requestKeyFrame()Resterende frames laten vallen + schone IDR aanvragen na configuratiewijziging
PTS-deduplicatietimestamp < lastRenderedPtsBuiten-volgorde frames laten vallen
FrameId-gatframeId > lastFrameId + 1ACK-vrije pakketverliesdetectie
desynchronized ContextWebGL2 desynchronized: trueCompositor omzeilen, 1 frame latentie besparen
Expliciet Snel FalensendScreenMirrorControl gooit GraphQLErrorMeldt "toegankelijkheid uitgeschakeld" in plaats van invoer stilletjes te negeren

Verder Lezen

  • WebCodecs API — MDN-documentatie over de VideoDecoder/AudioDecoder-interfaces.