{"record":{"id":"e88cf6a1743b3020","repo":"stablyai/orca","slug":"scrcpy-frame-size-size-exceeds-max-frame-byte","errorCode":null,"errorMessage":"scrcpy frame size ${size} exceeds ${MAX_FRAME_BYTES}; stream desynced","messagePattern":"scrcpy frame size (.+?) exceeds (.+?); stream desynced","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/main/emulator/android/scrcpy-video-frame-parser.ts","lineNumber":63,"sourceCode":"  keyFrame: boolean\n  pts: bigint\n  data: Buffer\n}\n\nexport type ScrcpyFrameParseResult = { frames: ScrcpyVideoFrame[]; pending: Buffer }\n\n// Extracts complete frames from `pending + chunk`, returning the leftover bytes\n// of any partially-received frame so the caller can prepend them to the next chunk.\nexport function parseScrcpyVideoFrames(pending: Buffer, chunk: Buffer): ScrcpyFrameParseResult {\n  const buffer = pending.length > 0 ? Buffer.concat([pending, chunk]) : chunk\n  const frames: ScrcpyVideoFrame[] = []\n  let offset = 0\n\n  while (buffer.length - offset >= FRAME_HEADER_SIZE) {\n    const meta = buffer.readBigUInt64BE(offset)\n    const size = buffer.readUInt32BE(offset + 8)\n    if (size > MAX_FRAME_BYTES) {\n      throw new Error(`scrcpy frame size ${size} exceeds ${MAX_FRAME_BYTES}; stream desynced`)\n    }\n    const dataStart = offset + FRAME_HEADER_SIZE\n    if (buffer.length - dataStart < size) {\n      break\n    }\n    frames.push({\n      config: (meta & CONFIG_FLAG) !== 0n,\n      keyFrame: (meta & KEY_FRAME_FLAG) !== 0n,\n      pts: meta & PTS_MASK,\n      data: Buffer.from(buffer.subarray(dataStart, dataStart + size))\n    })\n    offset = dataStart + size\n  }\n\n  return { frames, pending: offset > 0 ? Buffer.from(buffer.subarray(offset)) : buffer }\n}\n","sourceCodeStart":45,"sourceCodeEnd":80,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/emulator/android/scrcpy-video-frame-parser.ts#L45-L80","documentation":"Thrown by parseScrcpyVideoFrames when a frame's declared size field exceeds MAX_FRAME_BYTES (16 MiB). scrcpy frames at the configured max_size are far below this ceiling, so an oversized value indicates the byte stream is desynced — the parser is reading a size field at the wrong offset. Failing fast prevents unbounded buffering toward OOM.","triggerScenarios":"parseScrcpyVideoFrames(pending, chunk) reads the 12-byte frame header (8-byte meta + 4-byte size) at an offset that is not actually a frame boundary. This happens when earlier bytes were dropped, duplicated, or misinterpreted — e.g. the codec meta header was not stripped, a control-protocol byte leaked into the video stream, or the scrcpy server version uses a different frame-meta layout.","commonSituations":"A scrcpy server version mismatch (the pinned server jar expects send_codec_meta + send_frame_meta v2.4; a different version changes the header); the dummy byte / 64-byte device name handshake was not consumed before video parsing; a socket reconnection that lost partial frame bytes; stream corruption from an adb tunnel issue.","solutions":["Confirm the local scrcpy server jar version matches the protocol assumptions (codec meta + frame meta v2.4).","Verify the connection handshake (dummy byte + 64-byte device name) is fully consumed before the first video chunk reaches the parser.","On a desync, tear down the ScrcpyStreamSession and re-establish it rather than resuming mid-stream.","Ensure no control-channel bytes are mixed into the video socket."],"exampleFix":"// before: resuming a parser after a socket error corrupted offsets\nconst { frames, pending } = parseScrcpyVideoFrames(stalePending, chunk)\n// after: reset pending on reconnect and re-run the handshake\nif (reconnected) { pending = Buffer.alloc(0); await consumeHandshake(videoSocket) }\nconst { frames, pending: next } = parseScrcpyVideoFrames(pending, chunk)","handlingStrategy":"try-catch","validationCode":"// Reset parser state whenever the socket (re)connects so offsets stay aligned.\nfunction freshParserState(): Buffer { return Buffer.alloc(0) }","typeGuard":"function isStreamDesync(e: unknown): e is Error {\n  return e instanceof Error && /stream desynced/i.test(e.message)\n}","tryCatchPattern":"let pending = Buffer.alloc(0)\nfor await (const chunk of videoSocket) {\n  try {\n    const res = parseScrcpyVideoFrames(pending, chunk)\n    pending = res.pending\n    res.frames.forEach(cb.onFrame)\n  } catch (e) {\n    if (isStreamDesync(e)) { session.close(); throw e } // re-establish, do not resume\n    throw e\n  }\n}","preventionTips":["Fully consume the handshake (dummy byte + 64-byte name + codec meta) before parsing frames.","Reset pending bytes on any socket reconnect; never resume a desynced parser.","Keep the local scrcpy server jar version matched to the protocol assumptions (v2.4)."],"tags":["scrcpy","video","stream-desync","protocol","android"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}