AndroidSurfaceVideoRenderer
Draws frames into a Surface the caller owns.
The conversion from the decoder's pixel format to RGBA is supplied by the caller through convert, because it lives in whichever backend produced the frame. This module knows how to put pixels on a Surface and nothing about how they were decoded, which is why the converter arrives as a function rather than as a dependency.
The Surface belongs to the caller
The constructor stores the Surface and never calls release() on it, because it never owned it: it belongs to whichever view, window or codec handed it over, and that owner releases it on its own schedule. The one rule the caller has to keep is the other side of the same coin: close this renderer before releasing or replacing the Surface. close waits for a lock or a post already in flight, so once it returns nothing here will touch the Surface again. Releasing it first instead leaves this renderer drawing into a dead handle, which on Android is not an exception but a native abort.
Why it has its own worker
Converting a frame on the CPU and pushing it through a software canvas costs milliseconds, and at 1080p enough of them that doing it inside present starves everything: the presentation schedule slips, and with the process saturated the audio feeder cannot keep the device fed either. So present hands the frame to a worker and returns at once, which is what the renderer contract allows for exactly this reason.
Only the newest frame is kept. When the renderer cannot keep up, a new frame replaces the one still waiting rather than a queue building, which is the right trade for a slow renderer: the engine's schedule stays intact and the picture updates as often as this renderer manages, instead of the whole player being dragged down to its speed. The one slot is written out by hand rather than taken from a conflated channel, because a conflated channel discards the displaced element silently, and a silently discarded frame is a decoder buffer that is never closed and never counted. Owning the slot means the displaced frame is closed and counted, which is both correct and visible in supersededFrames.
One seam for Android graphics
Every Surface, Canvas and Bitmap call sits behind CanvasTarget, and everything above that seam is ordinary Kotlin: the ownership rules, the byte validation, the swizzle and the geometry. That is what lets the whole of this class be tested on a development machine, where the Android graphics classes exist as stubs that throw the moment they are called. The real Surface is proved separately, once, by a device test.
The rules this obeys
The frame belongs to this renderer from the moment present is called and is closed exactly once, including when it is superseded before being drawn, when conversion fails, and when the renderer is closed while it is still in hand. Losing the Surface is an event and not an exception: the renderer refuses frames while it is gone, counts them, says so once through events, and starts drawing again by itself when a lock next succeeds. It never calls back into the player and never stops playback, because a backgrounded window should not stop the sound.
Constructors
The renderer as a caller builds it: pictures are drawn into surface from this renderer's own thread.
Builds a renderer before its display Surface exists.
Properties
Frames that reached no Surface for a reason other than being superseded: a conversion that failed or returned the wrong number of bytes, a frame refused while the Surface was gone, a lock or a post the Surface would not give, a draw that threw, or a frame still in hand when the renderer closed. From Android 14 it also counts a hardware frame the display dropped after its release, because a newer frame was shown in its place.
The size the engine lays subtitles out for, rule 2 of docs/subtitle-placement.md. When a separate layer draws them, it is the size the host gave through setViewport. Otherwise it is the canvas this renderer draws them into, known from the first frame, and the host's size before that.
Frames the display showed. From Android 14 a hardware frame counts when MediaCodec reports it rendered. Before that, and for every software frame, it counts when its picture was posted to the Surface, which the display can still drop.
True while the picture on screen came through the software path, whose pixels this renderer keeps and draws again at a change of its look, playing or not (#541). False after a MediaCodec frame, which goes to the Surface and leaves no copy, so a paused picture takes a change of its look only from the engine decoding it again (#463).
True when HDR shows as HDR here (#447): under HdrPolicy.Auto, on the direct codec path, which hands the codec's HDR to the system, and on a display that says it supports PQ or HLG. The software path tone maps every frame, and a display the view has not described yet counts as standard range, so the variant choice keeps SDR until the view says otherwise.
Frames replaced in the waiting slot by a newer one before they could be drawn, or let go because the picture was taken off before they were drawn.
Functions
Takes the picture off (#530): the frame waiting for the worker goes, a frame the worker is converting is not drawn, and the worker blanks the Surface. Nothing here waits, because the engine calls this from its own loop.
What the display this renderer's Surface is on can show of HDR: the Display.HdrCapabilities types it supports and how far beyond standard white it goes now, or 1 when unknown. Fed by the view, because a Surface does not know its display. Until it is fed, the renderer reports nothing about HDR on the direct path, where only the display decides.
HDR from the decoder goes to the Surface as it is under HdrPolicy.Auto, and the system shows it as HDR on a display that can. Under HdrPolicy.ToneMap the next open asks MediaCodec for SDR, and a software frame is tone mapped by the converter either way.
Stores the overlay for the worker to composite above every following picture. A software picture is drawn again with it at once, however long the picture is held (#541); a MediaCodec picture's cues are drawn by the layer above it.
Replaces the display Surface without replacing this renderer or its paired decoder.
The size of the surface the overlay is drawn on, from its host. KitePlayerView gives the size of its subtitle layer here, which only the host knows while the codec owns the video Surface.
MediaCodec buffers go straight to the Surface; every other format uses the software fallback.