Common Video Calls Bugs and How to Catch Them
Common Video Calls Bugs and How to Catch Them
Common Video Calls Bugs and How to Catch Them
Video conferencing has become a core feature sets of the customer support portals to internal team tools. When a video call feature misbehaves, users notice immediately—dropped audio, frozen video, or a broken screen‑share flow can erode trust and push people to alternatives. This guide walks through the most frequent defects that surface in real‑world video‑call implementations, explains why they occur, shows how to reproduce them reliably, and gives concrete steps to fix and prevent each class of issue.
---
Common Video Calls Bugs and How to Catch Them: Audio Glitches
Audio problems are the most visible class of failure because users can hear them instantly. Typical symptoms include echo, one‑way audio, clipping, or sudden drops to silence. These glitches often stem from mismatched sample rates, incorrect handling of the WebRTC getUserMedia constraints, or race conditions when switching between microphone and speaker devices.
Why audio glitches happen
When a browser or native SDK initializes an audio track, it negotiates a sample rate (commonly 48 kHz for WebRTC). If the capture device reports a different rate—say 44.1 kHz from a USB headset—and the application forces a resample without proper buffering, the audio pipeline can underrun or overrun, producing clicks or silence. Another frequent cause is the failure to release the previous audio stream before requesting a new one, leaving the audio module in a locked state on some Android OEM skins.
Reproducing the bug
- Connect two clients: one using a USB headset that defaults to 44.1 kHz, the other using the built‑in microphone (48 kHz).
- In the call, invoke
getUserMedia({ audio: { sampleRate: 48000 } })on the headset client without checking the actualmediaStreamTrack.getSettings().sampleRate. - Observe the remote participant hearing periodic clicks every second or a brief mute after the first few seconds.
A scripted test that always uses the default microphone will miss this because it never encounters the mismatched device.
Detection strategies
- WebRTC stats: Pull
outbound-rtpandinbound-rtpstats and monitoraudioOutputLevelandtotalSamplesReceived. A sudden drop to zero whilebytesSentremains non‑zero indicates a silent‑audio condition. - Automated audio fingerprint: Play a known tone (e.g., 1 kHz sine wave) locally, capture the remote audio, and compute cross‑correlation. A correlation below 0.6 signals distortion or loss.
Fix and prevention
- Always read the actual settings returned by
getUserMediaand adapt the encoder/decoder pipeline accordingly. - Before opening a new stream, call
track.stop()on any existing audio track and await theendedevent. - Add a unit test that enumerates all audio devices reported by
navigator.mediaDevices.enumerateDevices()and runs the call flow with each combination of sample rates.
Code snippet – safe audio track swap (JavaScript)
let audioTrack = null;
async function switchAudioDevice(deviceId) {
if (audioTrack) {
audioTrack.stop();
await new Promise(res => audioTrack.addEventListener('ended', res));
}
const stream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: { exact: deviceId } }
});
audioTrack = stream.getAudioTracks()[0];
myPeerConnection.addTrack(audioTrack, stream);
}
---
Common Video Calls Bugs and How to Catch Them: Video Freeze
A frozen video frame while audio continues is a classic sign that the video pipeline stalled while the audio path stayed healthy. Users see a static image that may linger for seconds or minutes, often accompanied by a “reconnecting” badge that never clears.
Root causes
Video freeze usually originates from one of three places:
- Encoder overload – The CPU cannot keep up with the requested resolution/frame‑rate, causing the encoder to drop frames and eventually stall.
- Packet loss burst – A sudden loss of UDP packets (common on congested Wi‑Fi) leads to missing key frames; the decoder waits for the next I‑frame and shows the last decoded picture.
- Graphics texture leak – In native apps, each decoded frame is uploaded to a GPU texture; if the texture is not released, the GPU runs out of memory and stops accepting new frames.
Reproducing the freeze
- Encoder overload: On a low‑end Android emulator, set the video constraints to
{ width: 1920, height: 1080, frameRate: 30 }and run a call for two minutes. The software VP8 encoder will max out CPU and freeze. - Packet loss: Use
tcon Linux to add a 10 % loss rate on the UDP port used by WebRTC (tc qdisc add dev eth0 root netem loss 10%). Observe the video freezing after the first loss burst. - Texture leak: In an Android app using SurfaceView, deliberately omit
surface.release()after each frame render in a loop; after ~500 frames the video stalls.
Detection strategies
- WebRTC stats: Track
framesEncoded,framesDecoded,framesDropped, andtotalDecodeTime. A risingframesDroppedcoupled with stagnantframesDecodedindicates freeze. - GPU memory: On Android, query
adb shell dumpsys gfxinfoand watch theGL texture memoryvalue; a monotonic increase points to a leak. - Visual regression: Capture a short video of the local preview and compare frame‑by‑frame with a reference using SSIM; a sudden SSIM drop to near‑zero while audio RMS stays constant flags a freeze.
Fix and prevention
- Dynamically downgrade resolution/frame‑rate based on CPU usage (WebRTC’s
getStatsprovidescpuUsage). - Implement a key‑frame request (PLI) retry loop with exponential back‑off when
framesDecodedstalls for >500 ms. - Ensure every decoded frame’s texture is released; use try‑finally blocks or Kotlin’s
usescope.
Command – simulate packet loss for WebRTC (Linux)
# Replace eth0 with the interface carrying UDP traffic
sudo tc qdisc add dev eth0 root netem loss 10% delay 20ms
# After test, restore normal state
sudo tc qdisc del dev eth0 root
---
Common Video Calls Bugs and How to Catch Them: Screen Share Failures
Screen sharing introduces a second video source that often uses a different capture pipeline (desktop capture API, media projection, or broadcast intent). Failures appear as a black rectangle, a frozen shared view, or the caller seeing their own camera instead of the shared screen.
Why screen share breaks
- Permission prompts not handled – On macOS Catalina+, the user must grant “Screen Recording” access; if the automation clicks the wrong button or dismisses the dialog, the stream returns empty frames.
- Surface texture conflict – Some Android manufacturers overlay a secure flag on the surface used for media projection, causing the shared surface to be black when the app lacks the
CAPTURE_SECURE_VIDEO_OUTPUTpermission. - Frame‑rate mismatch – The shared desktop may be captured at 30 fps while the call expects 15 fps; the encoder drops frames, leading to noticeable stutter or a black screen after buffering exhaustion.
Reproducing the failure
- Permission mishandling: On a macOS test machine, disable “Screen Recording” for the test app in System Preferences → Privacy & Security. Initiate a share; the remote participant sees a black tile.
- Secure surface: On an Android device with Samsung Secure Folder enabled, start a media projection without requesting
android.permission.CAPTURE_SECURE_VIDEO_OUTPUT. The shared view appears black. - Frame‑rate stress: Set the capture frame rate to 60 fps via
navigator.mediaDevices.getDisplayMedia({ video: { frameRate: { ideal: 60 } } })while limiting the encoder to 15 fps. After ~30 seconds the shared video freezes.
Detection strategies
- Pixel checksum: Compute a running checksum of the shared video frames; a constant zero checksum indicates a black frame.
- Permission audit: After calling
getDisplayMedia, inspect the returnedMediaStreamTrack’senabledflag and thecontentHint; ifenabledis false, treat as a permission failure. - Encoder feedback: Monitor
framesEncodedandframesDroppedfor the shared track; a sudden spike in dropped frames followed by zero encoded frames signals encoder overload.
Fix and prevention
- Explicitly request and verify screen‑recording permissions before invoking
getDisplayMedia. Provide a clear UI fallback if denied. - On Android, check
ActivityManager.isScreenCaptureSupported()and request the secure output permission if needed. - Clamp the capture frame rate to the encoder’s maximum via
getCapabilities()or apply a simulcast layer that matches the encoder’s limits.
Snippet – permission‑aware screen share (Web)
async function startShare() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: "always" }
});
const track = stream.getVideoTracks()[0];
if (!track.enabled) {
throw new Error('Screen capture denied');
}
// optional: force frame rate to match encoder
track.applyConstraints({ frameRate: { max: 15 } });
return track;
} catch (e) {
console.error('Screen share failed:', e);
return null;
}
}
---
Common Video Calls Bugs and How to Catch Them: Network Latency Impact
High round‑trip time (RTT) and jitter manifest as delayed lip‑sync, frozen video, or audio that sounds “underwater.” While some latency is inevitable on cellular or satellite links, bugs in the congestion control or jitter buffer can turn tolerable delay into unusable quality.
Latency‑related failure modes
- Jitter buffer underrun – If the buffer is too small, a burst of jitter causes the decoder to starve, leading to audible gaps.
- Over‑aggressive bitrate reduction – The congestion controller may cut the video bitrate to near‑zero on a single spike, causing the video to freeze while audio continues.
- Incorrect RTT estimation – Using the wrong socket (e.g., measuring TCP RTT for UDP media) yields poor bandwidth predictions, resulting in either excessive packet loss or wasted bandwidth.
Reproducing latency issues
- Use
netemto add delay and jitter:sudo tc qdisc add dev eth0 root netem delay 200ms distribution normal 20ms 25%. - Run a call and observe the audio “ robotic ” quality when jitter exceeds the buffer size (typically 30 ms).
- To trigger aggressive bitrate reduction, add a 5 % loss burst every 10 seconds (
netem loss 5% correlation 25%) and watch the video bitrate plummet ingetStats.
Detection strategies
- Stats monitoring: Track
googJitterBufferMs,googJitterReceivedMs, andgoogTargetDelayMs. IfgoogJitterBufferMsstays near zero whilegoogJitterReceivedMsspikes, the buffer is under‑sized. - Audio MOS estimation: Use a lightweight PESQ approximation on the incoming audio stream; a MOS < 3.0 correlates with perceptible latency problems.
- Video freeze detector: As described earlier, combine
framesDecodedstagnation with risingrtt.
Fix and prevention
- Tune the jitter buffer size dynamically based on measured jitter (WebRTC allows
googJitterBufferMin/maxvia experimental flags). - Implement a minimum video bitrate floor (e.g., 100 kbps) to prevent the encoder from starving completely.
- Validate RTT measurement by comparing
googRttagainst ICMP ping to the same server; discard outliers.
Command – add jitter and delay for testing
# 150 ms base delay, 30 ms jitter (Gaussian)
sudo tc qdisc add dev eth0 root netem delay 150ms 30ms distribution normal
---
Common Video Calls Bugs and How to Catch Them: Permission Handling
Modern browsers and mobile OSes require explicit user consent for camera, microphone, screen capture, and sometimes location (for regional servers). Mishandling these dialogs leads to silent failures where the call starts but no media flows.
Typical permission bugs
- Assuming immediate grant – Code calls
getUserMediaand proceeds without awaiting the promise, using an undefined stream when the user delays or denies. - Not re‑requesting after denial – Some apps cache a denied state and never show the permission rationale again, leaving the user stuck.
- Incorrect permission name on Android – Using
CAMERAinstead ofandroid.permission.CAMERAin the manifest causes a silent failure on certain API levels.
Reproducing permission bugs
- Delayed grant: In a test harness, mock
navigator.mediaDevices.getUserMediato return a pending promise that resolves after 3 seconds. Start the call immediately; the local video element shows a black frame. - Denial path: Pre‑set the browser permission to “Block” for camera and microphone. Attempt to start a call; the app logs “Failed to get user media” but continues to show a connecting UI.
- Android manifest mistake: Build an APK with
(missing theandroid.prefix). On a Pixel 6, the camera opens but returns all‑black frames.
Detection strategies
- Promise inspection: Wrap
getUserMediain a utility that rejects if the promise does not settle within a configurable timeout (e.g., 2 seconds). - State machine verification: Model the call startup as a finite state machine (idle → requesting → granted → streaming). Any transition to “granted” without a prior “requested” state flags a logic error.
- Manifest lint: Run
aapt dump permissionsand verify that each used permission appears exactly as declared.
Fix and prevention
- Always
awaitthe media device promise and handle both success and error branches, showing a clear UI prompt to the user. - On denial, show a modal that explains why the permission is needed and provides a button to redirect to system settings (
openAppSettings()on Android,UIApplicationOpenSettingsURLStringon iOS). - Use automated tooling like
gradle androidLintoreslint-plugin-importwith a custom rule to enforce the correct permission strings.
Snippet – robust media request with timeout (JS)
function getUserMediaWithTimeout(constraints, timeout = 2000) {
return Promise.race([
navigator.mediaDevices.getUserMedia(constraints),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('getUserMedia timeout')), timeout)
)
]);
}
// Usage
getUserMediaWithTimeout({ video: true, audio: true })
.then(stream => attachStream(stream))
.catch(err => {
showPermissionDeniedUI(err);
console.error(err);
});
---
Common Video Calls Bugs and How to Catch Them: Device Orientation
When a user rotates their phone or switches a laptop from landscape to portrait, the video orientation can become upside‑down, mirrored, or stretched. This is especially disruptive in multi‑person grids where each tile expects a consistent orientation.
Why orientation bugs appear
- Missing rotation metadata – Some capture APIs deliver frames with a rotation flag (e.g., Android’s
Camera2providesCaptureResult.JPEG_ORIENTATION). If the encoder or renderer ignores this flag, the picture is drawn incorrectly. - CSS transform conflicts – In web apps, developers may apply a
transform: rotate(90deg)to mirror the preview but forget to inverse the transform for the outgoing stream, causing the remote participant to see a rotated image. - Hardware encoder limitations – Certain hardware VP8/H.264 encoders only support landscape orientation; feeding them a portrait frame forces the driver to rotate in software, dropping frames and causing stutter.
Reproducing orientation failures
- Android Camera2: Open the camera in portrait mode, capture a frame, and inspect the
CaptureResult.JPEG_ORIENTATION(expect 90). Feed the raw byte buffer to a WebRTC encoder that does not apply the rotation; the remote view appears rotated 90°. - Web CSS mistake: In a React component, set
style={{ transform: 'rotate(90deg)' }}on the local video element but omit the same transform on thecanvasused forgetUserMediaoutput. The local preview looks correct, but the remote sees a sideways image. - Encoder limit: On a low‑end Android device, force the video track to
{ width: 720, height: 1280 }(portrait) and enable the hardware encoder viamediacodecInfo.isHardwareAccelerated(). Observe dropped frames ingetStats.
Detection strategies
- Metadata check: After receiving a frame, extract any rotation metadata (Android
MediaFormat.KEY_ROTATION, WebRTCgoogFrameHeightInputvsgoogFrameHeightOutput). A mismatch > 0 flags ignored rotation. - Visual oracle: Render a known asymmetrical pattern (e.g., an “L” shape) in the preview, capture the outgoing stream, and use template matching to verify the pattern’s orientation.
- Encoder capability query: Before setting constraints, call
navigator.mediaDevices.getSupportedConstraints()and check ifaspectRatioincludes the desired portrait ratio; if not, fall back to software encoder.
Fix and prevention
- Always apply the rotation flag supplied by the capture pipeline before feeding frames to the encoder (Android: use
ImageReader.setOnImageAvailableListenerand rotate theImagewith aMatrix). - In web apps, keep a single source of truth for orientation: apply CSS transforms only to the preview element, not to the media stream; use
video.transform =orcanvas.getContext('2d').rotate()if you need to rotate the captured frames. - When targeting hardware encoders, enforce a landscape aspect ratio (e.g., 16:9) and let the UI handle letterboxing or pillarboxing for portrait devices.
Code – apply Camera2 rotation before encoding (Java)
private final ImageReader imageReader = ImageReader.newInstance(
width, height, ImageFormat.YUV_420_888, 2);
imageReader.setOnImageAvailableListener(reader -> {
Image img = reader.acquireLatestImage();
if (img == null) return;
int rotation = img.getTransformMatrix().getRotation(); // pseudo‑API
Plane[] planes = img.getPlanes();
ByteBuffer yBuffer = planes[0].getBuffer();
ByteBuffer uBuffer = planes[1].getBuffer();
ByteBuffer vBuffer = planes[2].getBuffer();
// Rotate YUV buffers if needed (simplified)
if (rotation == 90 || rotation == 270) {
// swap width/height and re‑index buffers
// ... implementation omitted for brevity
}
encoder.submitInputBuffer(yBuffer, uBuffer, vBuffer);
img.close();
}, handler);
---
Common Video Calls Bugs and How to Catch Them: Background Noise Suppression
Noise suppression (NS) aims to remove keyboard clicks, fan hum, or background chatter. When the NS algorithm is too aggressive, it can cut off speech onsets, producing a “robotic” or “choppy” voice. When it is too weak, users hear distracting noises that reduce intelligibility.
Failure patterns
- Over‑suppression – The NS gate cuts off the first 50‑150 ms of each spoken syllable, making consonants like /p/, /t/, /k/ sound missing.
- Under‑suppression – Background music or TV bleed-through remains audible, especially in low‑bitrate modes where the codec allocates fewer bits to audio.
- State‑machine glitch – Some NS implementations keep a long‑term noise estimate that never resets when the environment changes abruptly (e.g., moving from a quiet office to a noisy café).
Reproducing NS defects
- Over‑suppression: Play a recording of a sentence with sharp plosives at –30 dBm, add white noise at –20 dBm, and feed the mix into the mic. Listen to the output; the initial burst of each plosive will be attenuated.
- Under‑suppression: Run a call while a YouTube video plays at 60 dB SPL in the background. With NS disabled or set to “low”, the remote participant hears the video audio clearly.
- State glitch: Start a call in a silent room (noise floor –60 dB), then suddenly turn on a blender (≥ 40 dB). Observe that the NS continues to suppress speech as if the blender were still absent for ~2 seconds.
Detection strategies
- Spectral flux measurement: Compute the short‑term spectral flux of the incoming audio; a sudden drop in flux coinciding with speech onset indicates over‑suppression.
- Energy ratio: Compare the short‑term energy of the signal in the 500‑4000 Hz band (speech) to the total energy. A ratio below 0.3 during voiced frames suggests excessive suppression.
- Environment change detector: Track the long‑term noise estimate (if exposed via
getStatsgoogNoiseReductionInputLevel). A lag > 1 second after a known step change flags a sluggish NS.
Fix and prevention
- Use a voice activity detector (VAD) with a short hang‑time (≈ 150 ms) before applying gain reduction, preserving speech transients.
- Allow the user to select NS mode (off, low, medium, high) and persist the choice; expose a simple slider in the UI.
- Implement an adaptive noise floor that decays with a time constant of ~200 ms when speech is detected, preventing the estimate from sticking to outdated values.
Snippet – simple VAD‑based NS pseudo‑code (C)
float noise_estimate = INITIAL_NOISE;
float speech_prob = 0.0f;
const float alpha_up = 0.9f; // adapt when speech likely
const float alpha_down = 0.99f; // adapt when noise likely
for each frame {
float frame_energy = computeEnergy(frame);
speech_prob = vad(frame); // 0..1
if (speech_prob > 0.5) {
// speech present: adapt noise estimate slowly downward
noise_estimate = alpha_down * noise_estimate +
(1.0f - alpha_down) * frame_energy;
} else {
// likely noise: adapt upward quickly
noise_estimate = alpha_up * noise_estimate +
(1.0f - alpha_up) * frame_energy;
}
float gain = 1.0f / (1.0f + expf((noise_estimate - frame_energy) * K));
applyGain(frame, gain);
}
---
Common Video Calls Bugs and How to Catch Them: Chat Overlay Issues
Many video‑call apps overlay a text chat pane, participant list, or reaction emojis on top of the video feed. Bugs here manifest as the chat covering the speaker’s face, text rendering incorrectly on certain DPI scales, or the overlay stealing pointer events and preventing UI interaction with the call controls.
Why chat overlays break
- Z‑index mishandling – The chat container is given a higher z‑index than the video element, but the video is rendered inside a
that creates its own stacking context, causing the chat to appear underneath on some browsers. - Hardware acceleration conflict – When the video uses WebGL for rendering, the overlay drawn with regular HTML can lose sub‑pixel anti‑aliasing, leading to blurry text on high‑DPI screens.
- Pointer‑event passthrough – The overlay captures
pointerdownevents, stopping the underlying video container from receiving drag‑to‑pan gestures needed for screen‑share region selection.
Reproducing overlay bugs
- Z‑index: Create a test page with a
inside aand an overlaid. Setvideo { position: absolute; z-index: 2; }andchat { position: absolute; z-index: 1; }. In Chrome 115, the chat appears below the video because the video’s rendering layer creates a new stacking context.- DPI scaling: On a Windows laptop with 150 % scaling, set the chat font size to 14 px. The rendered text appears fuzzy because the browser rounds sub‑pixel positions when the parent container uses a transform.
- Pointer capture: Add a chat input that listens to
pointerdownand callsevent.stopPropagation(). Try to drag the video to reposition the picture‑in‑picture pane; the drag never starts.Detection strategies
- Layer inspection: Use Chrome DevTools → Layers panel to verify that the chat layer is above the video layer. Automate via Puppeteer:
await page.evaluate(() => document.documentElement.getClientRects())and compare z‑indices viagetComputedStyle. - Render test: Render a known string (e.g., “Hamburgerfonk”) in the chat, capture a screenshot, and run OCR to verify legibility; a character error rate > 5 % signals rendering problems.
- Event passthrough test: Simulate a
pointerdownon the video area via Puppeteer and assert that adragstartevent is fired on the video container.
Fix and prevention
- Place both video and chat inside a single flex container and use
orderto control visual stacking instead of relying on z‑index alone. - Disable hardware acceleration for the video element when overlaying HTML (
) to keep a shared compositing layer, or draw the chat into the same WebGL canvas. - Ensure any overlay that needs to be transparent to pointer events uses
pointer-events: none;on non‑interactive parts and only captures events on actual interactive widgets.
Snippet – safe overlay layout (CSS/HTML)
<div class="call-container"> <video id="local-video" autoplay playsinline></video> <div id="chat-pane" class="overlay"> <!-- chat messages --> </div> </div>.call-container { display: flex; position: relative; width: 100%; height: 100%; } #local-video { flex: 1; object-fit: cover; } #chat-pane { position: absolute; inset: 0; pointer-events: none; /* let clicks fall through to video */ display: flex; flex-direction: column; padding: 8px; } #chat-pane .input-box { pointer-events: auto; /* only the input captures */ }---
Common Video Calls Bugs and How to Catch Them: Recording Artifacts
Recording a call for later playback or compliance introduces a separate pipeline: the app must mux audio and video streams, often while simultaneously encoding for transmission. Recording bugs appear as desynchronized audio/video, missing frames, or files that refuse to open in standard players.
Sources of recording defects
- Muxer timestamp drift – If the audio and video encoders produce buffers with different clock bases (e.g., audio uses system clock, video uses capture timestamp), the muxer accumulates offset, leading to gradual drift.
- Insufficient buffering during network jitter – The recorder pulls from the incoming jitter buffer; if a network stall causes the buffer to empty, the recorder may drop a packet, creating a hole in the output file.
- Hardware encoder incompatibility – Some hardware H.264 encoders output Annex‑B byte streams while the recorder expects MP4‑style NAL units, producing a file that players cannot parse.
Reproducing recording problems
- Timestamp drift: Force the audio encoder to use a 44.1 kHz sample rate while the video encoder runs at 48 kHz (common when using different codecs). Record a 2‑minute call and inspect the resulting file with
ffprobe -show_frames; you’ll see the audio packets start later relative to video. - Jitter‑induced loss: Use
netemto add 300 ms delay with 10 % loss every 5 seconds. Record the call; the resulting MP4 will have a few seconds where the video freezes but audio continues, visible as a sudden PTS jump. - Annex‑B vs MP4: On an Android device, enable hardware H.264 via MediaCodec and configure the recorder to write directly to a file without converting to MP4. Try to play the file in VLC; it will fail with “Invalid data found when processing input”.
Detection strategies
- PTS monotonicity check: After muxing, run
ffprobe -select_streams v -show_entries frame=pkt_pts_time -of csvand verify that timestamps strictly increase. Any non‑monotonic sequence flags a muxer bug. - File integrity validation: Attempt to demuxing the file with
ffmpeg -v error -i input.mp4 -f null -and capture any non‑zero exit code. - Audio‑video sync metric: Compute the difference between the earliest audio PTS and the nearest video PTS for each second; a mean absolute difference > 40 ms indicates noticeable sync error.
Fix and prevention
- Use a single clock source (e.g., WebRTC’s
ntpNow()or Android’sSystem.nanoTime()) to timestamp both audio and video frames before handing them to the muxer. - Insert a small forward‑error‑correction (FEC) or retransmission layer for the recorder’s internal buffers, or simply pause recording briefly
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free