rustdesk/rustdesk · warning · anyhow::Error

no valid frame

Error message

no valid frame

What it means

VpxEncoder::encode_to_message encodes the input AND then calls flush(), collecting all produced frames; if after both steps the list is empty it returns Err('no valid frame'). Because flush() is already drained here (unlike the AOM path), an empty result means the encoder genuinely produced nothing for the input — almost always an invalid input or misconfigured encoder rather than latency.

Source

Thrown at libs/scrap/src/common/vpxcodec.rs:190

    }

    fn encode_to_message(&mut self, input: EncodeInput, ms: i64) -> ResultType<VideoFrame> {
        let mut frames = Vec::new();
        for ref frame in self
            .encode(ms, input.yuv()?, STRIDE_ALIGN)
            .with_context(|| "Failed to encode")?
        {
            frames.push(VpxEncoder::create_frame(frame));
        }
        for ref frame in self.flush().with_context(|| "Failed to flush")? {
            frames.push(VpxEncoder::create_frame(frame));
        }

        // to-do: flush periodically, e.g. 1 second
        if frames.len() > 0 {
            Ok(VpxEncoder::create_video_frame(self.id, frames))
        } else {
            Err(anyhow!("no valid frame"))
        }
    }

    fn yuvfmt(&self) -> crate::EncodeYuvFormat {
        self.yuvfmt.clone()
    }

    #[cfg(feature = "vram")]
    fn input_texture(&self) -> bool {
        false
    }

    fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
        let mut c = unsafe { *self.ctx.config.enc.to_owned() };
        let (q_min, q_max) = Self::calc_q_values(ratio);
        c.rc_min_quantizer = q_min;
        c.rc_max_quantizer = q_max;
        c.rc_target_bitrate = Self::bitrate(self.width as _, self.height as _, ratio);

View on GitHub (pinned to 7aa98d43cf)

Solutions

  1. Check the capture source: skip encode when the frame is empty or dimensions are 0 (e.g. during display transitions).
  2. Recreate the encoder whenever the capture width/height changes instead of reusing the old config.
  3. If it fires on the very first frame only, tolerate and continue — but since flush() ran, treat repeated occurrences as an input bug.
  4. Log input dimensions vs encoder config at the error site.

Example fix

// before
let vf = enc.encode_to_message(input, ms)?; // empty after encode+flush -> Err

// after: guard the capture side
let Ok(yuv) = input.yuv() else { continue; };
if yuv.dim == (0, 0) { continue; } // display switching, no data yet
let vf = enc.encode_to_message(input, ms)?;
Defensive patterns

Strategy: validation

Validate before calling

let (w, h) = (frame.width(), frame.height());
if w == 0 || h == 0 { continue; } // display transitioning / hotplug
if (w, h) != (enc.width as u32, enc.height as u32) { recreate_encoder(w, h)?; }

Try / catch

match enc.encode_to_message(input, ms) {
    Ok(vf) => send(vf),
    Err(e) if e.to_string() == "no valid frame" && first_few => continue,
    Err(e) => return Err(e), // flush already ran: persistent empties mean bad input
}

Prevention

When it happens

Trigger: input.yuv() returning an empty/zero-sized image; encoder width/height mismatched with the submitted frame so libvpx rejects it silently; encoder already in a failed state from an earlier control call.

Common situations: Screen capture returning a 0x0 frame during mode changes/monitor hotplug; resolution change after encoder creation without recreating the encoder; corrupted capture buffers.

Related errors


AI-assisted analysis of rustdesk/rustdesk@7aa98d43cf (2026-08-16). Data as JSON: /api/errors/10b86e9150362bff. Report an issue: GitHub.