Intermediate
20 min
Full Guide
WebRTC Media Streams
Capture and stream audio/video between peers for video calls and screen sharing
Media Capture APIs
WebRTC provides powerful APIs for capturing and streaming audio and video. The
getUserMedia API captures camera/microphone, while getDisplayMedia
captures screen content. These streams can then be sent peer-to-peer via WebRTC.
getUserMedia - Camera & Microphone
// Request access to camera and microphone
// Basic usage
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// Attach to video element
document.getElementById('localVideo').srcObject = stream;
return stream;
} catch (error) {
handleMediaError(error);
}
}
function handleMediaError(error) {
switch (error.name) {
case 'NotAllowedError':
alert('Permission denied. Please allow camera/microphone access.');
break;
case 'NotFoundError':
alert('No camera or microphone found.');
break;
case 'NotReadableError':
alert('Camera/microphone is already in use.');
break;
case 'OverconstrainedError':
alert('Requested constraints cannot be satisfied.');
break;
default:
console.error('Media error:', error);
}
}
// With detailed constraints
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { min: 640, ideal: 1280, max: 1920 },
height: { min: 480, ideal: 720, max: 1080 },
frameRate: { ideal: 30, max: 60 },
facingMode: 'user', // 'user' (front) or 'environment' (back)
aspectRatio: 16/9
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
sampleRate: 48000,
channelCount: 2
}
});
// Enumerate available devices
async function getDevices() {
const devices = await navigator.mediaDevices.enumerateDevices();
const cameras = devices.filter(d => d.kind === 'videoinput');
const microphones = devices.filter(d => d.kind === 'audioinput');
const speakers = devices.filter(d => d.kind === 'audiooutput');
console.log('Cameras:', cameras);
console.log('Microphones:', microphones);
console.log('Speakers:', speakers);
return { cameras, microphones, speakers };
}
// Select specific device
async function useCamera(deviceId) {
return navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: deviceId } },
audio: true
});
}
getDisplayMedia - Screen Sharing
// Capture screen, window, or browser tab
async function startScreenShare() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: 'always', // 'always', 'motion', or 'never'
displaySurface: 'monitor' // 'monitor', 'window', 'browser'
},
audio: true // System audio (if supported)
});
// User selected a source - attach to video
document.getElementById('screenVideo').srcObject = stream;
// Handle user stopping share via browser UI
stream.getVideoTracks()[0].onended = () => {
console.log('User stopped screen sharing');
handleScreenShareEnd();
};
return stream;
} catch (error) {
if (error.name === 'NotAllowedError') {
console.log('User cancelled screen share');
}
}
}
// Combine camera and screen (picture-in-picture effect)
async function createPipStream() {
const cameraStream = await navigator.mediaDevices.getUserMedia({
video: { width: 320, height: 180 },
audio: true
});
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: true
});
// Use Canvas to combine streams
const canvas = document.createElement('canvas');
canvas.width = 1920;
canvas.height = 1080;
const ctx = canvas.getContext('2d');
const screenVideo = document.createElement('video');
screenVideo.srcObject = screenStream;
await screenVideo.play();
const cameraVideo = document.createElement('video');
cameraVideo.srcObject = cameraStream;
await cameraVideo.play();
function draw() {
// Draw screen full size
ctx.drawImage(screenVideo, 0, 0, canvas.width, canvas.height);
// Draw camera in corner
ctx.drawImage(cameraVideo, canvas.width - 340, canvas.height - 200, 320, 180);
requestAnimationFrame(draw);
}
draw();
// Capture canvas as stream
const combinedStream = canvas.captureStream(30);
// Add audio from camera
combinedStream.addTrack(cameraStream.getAudioTracks()[0]);
return combinedStream;
}
Working with MediaStream
// MediaStream contains MediaStreamTracks
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
// Get tracks
const videoTracks = stream.getVideoTracks();
const audioTracks = stream.getAudioTracks();
const allTracks = stream.getTracks();
console.log('Video tracks:', videoTracks.length);
console.log('Audio tracks:', audioTracks.length);
// Track properties
const videoTrack = videoTracks[0];
console.log('Track ID:', videoTrack.id);
console.log('Track kind:', videoTrack.kind);
console.log('Track label:', videoTrack.label);
console.log('Track enabled:', videoTrack.enabled);
console.log('Track muted:', videoTrack.muted);
console.log('Track readyState:', videoTrack.readyState);
// Get current settings
const settings = videoTrack.getSettings();
console.log('Resolution:', settings.width, 'x', settings.height);
console.log('Frame rate:', settings.frameRate);
console.log('Device ID:', settings.deviceId);
// Get capabilities
const capabilities = videoTrack.getCapabilities();
console.log('Max resolution:', capabilities.width.max, 'x', capabilities.height.max);
// Mute/unmute (disable track)
function toggleVideo() {
videoTrack.enabled = !videoTrack.enabled;
}
function toggleAudio() {
audioTracks[0].enabled = !audioTracks[0].enabled;
}
// Stop tracks (release camera/mic)
function stopMedia() {
stream.getTracks().forEach(track => {
track.stop();
});
}
// Track events
videoTrack.onended = () => {
console.log('Track ended (device disconnected or stopped)');
};
videoTrack.onmute = () => {
console.log('Track muted');
};
videoTrack.onunmute = () => {
console.log('Track unmuted');
};
Adding Media to Peer Connection
// Send media tracks over WebRTC
const pc = new RTCPeerConnection(config);
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
// Add tracks to peer connection
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
console.log('Added track:', track.kind);
});
// Alternative: addStream (deprecated but still works)
// pc.addStream(stream);
// Handle incoming tracks from remote peer
pc.ontrack = (event) => {
console.log('Received track:', event.track.kind);
// event.streams contains the MediaStream(s)
const remoteStream = event.streams[0];
document.getElementById('remoteVideo').srcObject = remoteStream;
// Or create new stream from track
if (!remoteVideo.srcObject) {
remoteVideo.srcObject = new MediaStream();
}
remoteVideo.srcObject.addTrack(event.track);
};
// Get senders (for modifying tracks later)
const senders = pc.getSenders();
const videoSender = senders.find(s => s.track?.kind === 'video');
const audioSender = senders.find(s => s.track?.kind === 'audio');
// Replace track (e.g., switch camera)
async function switchCamera(newDeviceId) {
const newStream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: newDeviceId } }
});
const newTrack = newStream.getVideoTracks()[0];
await videoSender.replaceTrack(newTrack);
// Update local preview
localVideo.srcObject = newStream;
}
// Add screen share while keeping audio
async function addScreenShare() {
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const screenTrack = screenStream.getVideoTracks()[0];
// Replace video track with screen
await videoSender.replaceTrack(screenTrack);
// When screen share ends, switch back to camera
screenTrack.onended = async () => {
const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true });
await videoSender.replaceTrack(cameraStream.getVideoTracks()[0]);
};
}
Complete Video Call Implementation
// Full video call with all features
class VideoCall {
constructor(signalingSocket) {
this.socket = signalingSocket;
this.pc = null;
this.localStream = null;
this.remoteStream = null;
this.isMuted = false;
this.isCameraOff = false;
this.config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.example.com:3478',
username: 'user',
credential: 'pass'
}
]
};
this.setupSignaling();
}
setupSignaling() {
this.socket.on('offer', async ({ from, offer }) => {
await this.handleOffer(from, offer);
});
this.socket.on('answer', async ({ answer }) => {
await this.pc.setRemoteDescription(new RTCSessionDescription(answer));
});
this.socket.on('ice-candidate', async ({ candidate }) => {
if (candidate) {
await this.pc.addIceCandidate(new RTCIceCandidate(candidate));
}
});
this.socket.on('hang-up', () => {
this.endCall();
});
}
async startLocalMedia() {
this.localStream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720 },
audio: {
echoCancellation: true,
noiseSuppression: true
}
});
document.getElementById('localVideo').srcObject = this.localStream;
return this.localStream;
}
createPeerConnection() {
this.pc = new RTCPeerConnection(this.config);
// Add local tracks
this.localStream.getTracks().forEach(track => {
this.pc.addTrack(track, this.localStream);
});
// ICE candidates
this.pc.onicecandidate = (event) => {
if (event.candidate) {
this.socket.emit('ice-candidate', {
to: this.remotePeer,
candidate: event.candidate
});
}
};
// Connection state
this.pc.onconnectionstatechange = () => {
const state = this.pc.connectionState;
this.onConnectionStateChange?.(state);
if (state === 'failed' || state === 'closed') {
this.endCall();
}
};
// Remote stream
this.pc.ontrack = (event) => {
this.remoteStream = event.streams[0];
document.getElementById('remoteVideo').srcObject = this.remoteStream;
};
return this.pc;
}
async call(userId) {
this.remotePeer = userId;
await this.startLocalMedia();
this.createPeerConnection();
const offer = await this.pc.createOffer();
await this.pc.setLocalDescription(offer);
this.socket.emit('offer', {
to: userId,
offer: offer
});
}
async handleOffer(from, offer) {
this.remotePeer = from;
await this.startLocalMedia();
this.createPeerConnection();
await this.pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await this.pc.createAnswer();
await this.pc.setLocalDescription(answer);
this.socket.emit('answer', {
to: from,
answer: answer
});
}
toggleMute() {
this.isMuted = !this.isMuted;
this.localStream.getAudioTracks().forEach(track => {
track.enabled = !this.isMuted;
});
return this.isMuted;
}
toggleCamera() {
this.isCameraOff = !this.isCameraOff;
this.localStream.getVideoTracks().forEach(track => {
track.enabled = !this.isCameraOff;
});
return this.isCameraOff;
}
async shareScreen() {
const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const screenTrack = screenStream.getVideoTracks()[0];
const sender = this.pc.getSenders().find(s => s.track?.kind === 'video');
await sender.replaceTrack(screenTrack);
screenTrack.onended = async () => {
const cameraTrack = this.localStream.getVideoTracks()[0];
await sender.replaceTrack(cameraTrack);
};
}
hangUp() {
this.socket.emit('hang-up', { to: this.remotePeer });
this.endCall();
}
endCall() {
// Stop all tracks
this.localStream?.getTracks().forEach(track => track.stop());
// Close peer connection
this.pc?.close();
this.pc = null;
// Clear video elements
document.getElementById('localVideo').srcObject = null;
document.getElementById('remoteVideo').srcObject = null;
this.onCallEnded?.();
}
}
// Usage
const call = new VideoCall(socket);
call.onConnectionStateChange = (state) => {
console.log('Call state:', state);
};
call.onCallEnded = () => {
console.log('Call ended');
};
// Start call
await call.call('userId123');
// Controls
document.getElementById('muteBtn').onclick = () => {
const muted = call.toggleMute();
updateMuteButton(muted);
};
document.getElementById('cameraBtn').onclick = () => {
const off = call.toggleCamera();
updateCameraButton(off);
};
document.getElementById('screenBtn').onclick = () => {
call.shareScreen();
};
document.getElementById('hangUpBtn').onclick = () => {
call.hangUp();
};
Audio Processing and Analysis
// Use Web Audio API for advanced audio processing
async function createAudioProcessor(stream) {
const audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
// Create analyzer for visualization
const analyzer = audioContext.createAnalyser();
analyzer.fftSize = 256;
source.connect(analyzer);
// Volume meter
function getVolume() {
const data = new Uint8Array(analyzer.frequencyBinCount);
analyzer.getByteFrequencyData(data);
const sum = data.reduce((a, b) => a + b, 0);
return sum / data.length;
}
// Visualize
function drawWaveform(canvas) {
const ctx = canvas.getContext('2d');
const data = new Uint8Array(analyzer.frequencyBinCount);
function draw() {
analyzer.getByteFrequencyData(data);
ctx.fillStyle = 'rgb(0, 0, 0)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const barWidth = canvas.width / data.length;
data.forEach((value, i) => {
const height = (value / 255) * canvas.height;
ctx.fillStyle = `hsl(${i * 2}, 100%, 50%)`;
ctx.fillRect(i * barWidth, canvas.height - height, barWidth - 1, height);
});
requestAnimationFrame(draw);
}
draw();
}
return { getVolume, drawWaveform, analyzer };
}
// Noise gate (mute when not speaking)
function createNoiseGate(stream, threshold = 0.02) {
const audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
const analyzer = audioContext.createAnalyser();
const destination = audioContext.createMediaStreamDestination();
const gainNode = audioContext.createGain();
source.connect(analyzer);
source.connect(gainNode);
gainNode.connect(destination);
analyzer.fftSize = 256;
const data = new Float32Array(analyzer.fftSize);
function checkLevel() {
analyzer.getFloatTimeDomainData(data);
const rms = Math.sqrt(data.reduce((sum, x) => sum + x * x, 0) / data.length);
gainNode.gain.value = rms > threshold ? 1 : 0;
requestAnimationFrame(checkLevel);
}
checkLevel();
return destination.stream;
}
💡 Media Best Practices
- ✓ Request minimal permissions - Only ask for what you need
- ✓ Handle permission errors - Graceful fallback if denied
- ✓ Use replaceTrack - Avoid renegotiation when switching sources
- ✓ Stop tracks when done - Release camera/mic resources
- ✓ Enable echo cancellation - Essential for speakers + mic
- ✓ Provide visual feedback - Show mute/camera state clearly
- ✓ Test on mobile - Camera APIs behave differently