LiveKit Ingress Service - WebRTC Input (WHIP)
Mix.install([
{:livekit, path: "../.."},
{:kino, "~> 0.12"},
{:jason, "~> 1.4"}
])
Introduction to WebRTC Ingress (WHIP)
This Livebook explores WebRTC-HTTP Ingress Protocol (WHIP) for low-latency streaming with LiveKit. WHIP is a modern alternative to RTMP that offers:
- π Ultra-low latency (sub-second)
- π Better firewall traversal via WebRTC
- π Built-in security with DTLS/SRTP
- π± Browser-native support without plugins
- ποΈ Advanced codec support (AV1, VP9, H.264)
What Youβll Learn:
- π§ Setting up WHIP ingress endpoints
- π Browser-based streaming with JavaScript
- π± Mobile WebRTC streaming considerations
- β‘ Optimizing for ultra-low latency
- π Debugging WebRTC connections
- π Monitoring WebRTC performance
Configuration & Client Setup
# LiveKit configuration form
config_form = Kino.Control.form(
[
api_key: Kino.Input.password("LiveKit API Key"),
api_secret: Kino.Input.password("LiveKit API Secret"),
url: Kino.Input.text("LiveKit Server URL", default: "wss://your-server.livekit.cloud"),
room_name: Kino.Input.text("Target Room Name", default: "whip-demo-room")
],
submit: "Connect to LiveKit"
)
# Establish connection and store configuration
config = Kino.Control.read(config_form)
Process.put(:config, config)
case Livekit.IngressServiceClient.new(config.url, config.api_key, config.api_secret) do
{:ok, client} ->
Process.put(:client, client)
IO.puts("β
Connected to LiveKit Ingress Service!")
IO.puts("π― Target room: #{config.room_name}")
IO.puts("π Server: #{config.url}")
{:error, reason} ->
IO.puts("β Connection failed: #{reason}")
IO.puts("Please verify your credentials and server URL.")
end
Creating a WHIP Ingress Endpoint
Letβs create a WebRTC ingress endpoint optimized for low-latency streaming:
# WHIP Ingress Configuration Form
whip_form = Kino.Control.form(
[
name: Kino.Input.text("Stream Name", default: "whip-stream-#{System.system_time(:second)}"),
participant_identity: Kino.Input.text("Streamer Identity", default: "webrtc-streamer"),
participant_name: Kino.Input.text("Display Name", default: "WebRTC Broadcaster"),
participant_metadata: Kino.Input.textarea("Metadata (JSON)", default: """
{
"streaming_protocol": "WHIP",
"codec_preference": "VP9",
"latency_mode": "ultra_low",
"browser": "Chrome/Safari"
}"""),
enable_transcoding: Kino.Input.checkbox("Enable Transcoding", default: false)
],
submit: "Create WHIP Ingress"
)
# Create the WHIP ingress endpoint
whip_params = Kino.Control.read(whip_form)
client = Process.get(:client)
config = Process.get(:config)
# Validate and prepare metadata
metadata = try do
Jason.decode!(whip_params.participant_metadata)
whip_params.participant_metadata
rescue
_ -> "{\"note\": \"Invalid JSON, using fallback\"}"
end
request = %Livekit.CreateIngressRequest{
input_type: :WHIP_INPUT,
name: whip_params.name,
room_name: config.room_name,
participant_identity: whip_params.participant_identity,
participant_name: whip_params.participant_name,
participant_metadata: metadata,
enable_transcoding: whip_params.enable_transcoding
}
case Livekit.IngressServiceClient.create_ingress(client, request) do
{:ok, ingress} ->
Process.put(:whip_ingress, ingress)
IO.puts("π WHIP Ingress Created Successfully!")
IO.puts("=" |> String.duplicate(50))
IO.puts("π‘ Ingress ID: #{ingress.ingress_id}")
IO.puts("π·οΈ Name: #{ingress.name}")
IO.puts("π WHIP URL: #{ingress.url}")
IO.puts("π Room: #{ingress.room_name}")
IO.puts("π€ Participant: #{ingress.participant_identity}")
IO.puts("π Display Name: #{ingress.participant_name}")
IO.puts("βοΈ Transcoding: #{ingress.enable_transcoding}")
IO.puts("π Metadata: #{ingress.participant_metadata}")
IO.puts("=" |> String.duplicate(50))
IO.puts("")
IO.puts("β‘ Low-latency WebRTC endpoint ready!")
{:error, reason} ->
IO.puts("β Failed to create WHIP ingress: #{inspect(reason)}")
end
Browser WebRTC Streaming Code Generator
Letβs generate JavaScript code for browser-based WebRTC streaming:
ingress = Process.get(:whip_ingress)
if ingress do
IO.puts("π Browser WebRTC Streaming Code")
IO.puts("=" |> String.duplicate(50))
IO.puts("")
IO.puts("Copy and paste this HTML file to test WebRTC streaming:")
IO.puts("")
html_code = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebRTC WHIP Streaming to LiveKit</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
video {
width: 100%;
max-width: 640px;
border: 1px solid #ccc;
border-radius: 8px;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover { background: #0056b3; }
button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.status {
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
.status.success { background: #d4edda; color: #155724; }
.status.error { background: #f8d7da; color: #721c24; }
.status.warning { background: #fff3cd; color: #856404; }
</style>
</head>
<body>
<h1>π WebRTC WHIP Streaming to LiveKit</h1>
<div id="status" class="status warning">
π± Click "Start Camera" to begin streaming
</div>
<video id="localVideo" autoplay muted></video>
<div>
<button id="startBtn" onclick="startStreaming()">Start Camera</button>
<button id="stopBtn" onclick="stopStreaming()" disabled>Stop Streaming</button>
<button id="toggleAudio" disabled>Toggle Audio</button>
<button id="toggleVideo" disabled>Toggle Video</button>
</div>
<div>
<h3>π Stream Information:</h3>
<ul>
<li><strong>WHIP URL:</strong> <code>#{ingress.url}</code></li>
<li><strong>Room:</strong> #{ingress.room_name}</li>
<li><strong>Participant:</strong> #{ingress.participant_identity}</li>
<li><strong>Stream ID:</strong> #{ingress.ingress_id}</li>
</ul>
</div>
<script>
let localStream = null;
let peerConnection = null;
let isStreaming = false;
const whipUrl = '#{ingress.url}';
const statusDiv = document.getElementById('status');
const localVideo = document.getElementById('localVideo');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const toggleAudioBtn = document.getElementById('toggleAudio');
const toggleVideoBtn = document.getElementById('toggleVideo');
function updateStatus(message, type = 'warning') {
statusDiv.textContent = message;
statusDiv.className = `status ${type}`;
console.log(message);
}
async function startStreaming() {
try {
updateStatus('πΉ Requesting camera and microphone access...');
// Get user media
localStream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
localVideo.srcObject = localStream;
updateStatus('π± Camera started, setting up WebRTC connection...');
// Create peer connection
peerConnection = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
});
// Add tracks to peer connection
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream);
});
// Create and set local description
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
updateStatus('π Connecting to LiveKit via WHIP...');
// Send offer to WHIP endpoint
const response = await fetch(whipUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp'
},
body: offer.sdp
});
if (!response.ok) {
throw new Error(`WHIP request failed: ${response.status}`);
}
const answerSdp = await response.text();
await peerConnection.setRemoteDescription({
type: 'answer',
sdp: answerSdp
});
// Monitor connection state
peerConnection.onconnectionstatechange = () => {
const state = peerConnection.connectionState;
console.log('Connection state:', state);
switch(state) {
case 'connected':
updateStatus('β
Streaming live to LiveKit!', 'success');
break;
case 'disconnected':
case 'failed':
updateStatus('β Connection lost', 'error');
break;
case 'connecting':
updateStatus('π Connecting...', 'warning');
break;
}
};
// Update UI
isStreaming = true;
startBtn.disabled = true;
stopBtn.disabled = false;
toggleAudioBtn.disabled = false;
toggleVideoBtn.disabled = false;
} catch (error) {
console.error('Streaming error:', error);
updateStatus(`β Error: ${error.message}`, 'error');
stopStreaming();
}
}
function stopStreaming() {
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
localStream = null;
}
if (peerConnection) {
peerConnection.close();
peerConnection = null;
}
localVideo.srcObject = null;
isStreaming = false;
// Update UI
startBtn.disabled = false;
stopBtn.disabled = true;
toggleAudioBtn.disabled = true;
toggleVideoBtn.disabled = true;
updateStatus('βΉοΈ Streaming stopped');
}
function toggleAudio() {
if (localStream) {
const audioTrack = localStream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
toggleAudioBtn.textContent = audioTrack.enabled ?
'Mute Audio' : 'Unmute Audio';
updateStatus(audioTrack.enabled ?
'π Audio enabled' : 'π Audio muted', 'success');
}
}
}
function toggleVideo() {
if (localStream) {
const videoTrack = localStream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
toggleVideoBtn.textContent = videoTrack.enabled ?
'Disable Video' : 'Enable Video';
updateStatus(videoTrack.enabled ?
'πΉ Video enabled' : 'πΉ Video disabled', 'success');
}
}
}
// Update button text on load
toggleAudioBtn.textContent = 'Mute Audio';
toggleVideoBtn.textContent = 'Disable Video';
// Handle page unload
window.addEventListener('beforeunload', stopStreaming);
</script>
</body>
</html>
"""
IO.puts(html_code)
IO.puts("")
IO.puts("πΎ Save this as 'whip_streaming.html' and open in a web browser!")
IO.puts("π Make sure to allow camera/microphone permissions when prompted.")
else
IO.puts("β οΈ Please create a WHIP ingress first")
end
Advanced WebRTC Configuration
Letβs explore advanced WebRTC configurations for different use cases:
# Advanced WebRTC configuration generator
advanced_config_form = Kino.Control.form(
[
use_case: Kino.Input.select("Use Case", [
{"Ultra Low Latency Gaming", :gaming},
{"High Quality Broadcasting", :broadcasting},
{"Mobile-Optimized Streaming", :mobile},
{"Screen Sharing", :screen_share}
]),
video_codec: Kino.Input.select("Video Codec Preference", [
{"VP9 (Best Quality)", "VP9"},
{"VP8 (Good Compatibility)", "VP8"},
{"H.264 (Universal)", "H264"}
]),
audio_codec: Kino.Input.select("Audio Codec", [
{"Opus (Recommended)", "opus"},
{"G.722", "G722"},
{"PCMU", "PCMU"}
])
],
submit: "Generate Advanced Configuration"
)
# Generate advanced WebRTC configuration
advanced_params = Kino.Control.read(advanced_config_form)
config_js = case advanced_params.use_case do
:gaming ->
"""
// Gaming-optimized WebRTC configuration
const rtcConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};
const mediaConstraints = {
video: {
width: { exact: 1920 },
height: { exact: 1080 },
frameRate: { exact: 60 }, // High framerate for gaming
latency: { ideal: 0.01 } // Ultra-low latency
},
audio: {
echoCancellation: false, // Disable for gaming audio
noiseSuppression: false,
autoGainControl: false,
latency: { ideal: 0.01 }
}
};
"""
:broadcasting ->
"""
// Broadcasting-optimized WebRTC configuration
const rtcConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
const mediaConstraints = {
video: {
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 30 },
facingMode: 'user'
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
sampleRate: 48000,
channelCount: 2
}
};
"""
:mobile ->
"""
// Mobile-optimized WebRTC configuration
const rtcConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};
const mediaConstraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 },
facingMode: { ideal: 'user' } // Front camera by default
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
};
// Mobile-specific optimizations
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobile) {
mediaConstraints.video.width = { ideal: 854 };
mediaConstraints.video.height = { ideal: 480 };
mediaConstraints.video.frameRate = { ideal: 24 };
}
"""
:screen_share ->
"""
// Screen sharing WebRTC configuration
const rtcConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};
const mediaConstraints = {
video: {
cursor: 'always',
displaySurface: 'monitor',
frameRate: { ideal: 30 },
width: { ideal: 1920 },
height: { ideal: 1080 }
},
audio: false // Usually no system audio for screen share
};
// Use getDisplayMedia for screen capture
async function getScreenShare() {
return navigator.mediaDevices.getDisplayMedia(mediaConstraints);
}
"""
end
IO.puts("π§ Advanced WebRTC Configuration (#{advanced_params.use_case}):")
IO.puts("=" |> String.duplicate(60))
IO.puts("")
IO.puts(config_js)
IO.puts("")
IO.puts("π Codec Preferences:")
IO.puts(" Video: #{advanced_params.video_codec}")
IO.puts(" Audio: #{advanced_params.audio_codec}")
Real-time WebRTC Performance Monitor
Letβs create a monitoring system for WebRTC performance metrics:
defmodule WebRTCMonitor do
def check_ingress_status(client, ingress_id) do
case Livekit.IngressServiceClient.list_ingress(client) do
{:ok, response} ->
ingress = Enum.find(response.items, fn i -> i.ingress_id == ingress_id end)
if ingress do
%{
found: true,
ingress: ingress,
status: ingress.state && ingress.state.status,
room_id: ingress.state && ingress.state.room_id,
tracks: ingress.state && ingress.state.tracks || []
}
else
%{found: false, error: "Ingress not found"}
end
{:error, reason} ->
%{found: false, error: reason}
end
end
def format_status(status) do
case status do
:ENDPOINT_INACTIVE -> {"π΄", "Inactive"}
:ENDPOINT_BUFFERING -> {"π‘", "Buffering"}
:ENDPOINT_PUBLISHING -> {"π’", "Publishing"}
:ENDPOINT_ERROR -> {"β", "Error"}
:ENDPOINT_COMPLETE -> {"β
", "Complete"}
_ -> {"βͺ", "Unknown"}
end
end
end
# Create monitoring frame
monitor_frame = Kino.Frame.new()
# Start monitoring task
monitor_task = Task.async(fn ->
ingress = Process.get(:whip_ingress)
client = Process.get(:client)
if ingress && client do
Enum.each(1..20, fn iteration -> # Monitor for 100 seconds (20 * 5 seconds)
timestamp = DateTime.utc_now() |> DateTime.to_string()
status_info = WebRTCMonitor.check_ingress_status(client, ingress.ingress_id)
content = if status_info.found do
{emoji, status_text} = WebRTCMonitor.format_status(status_info.status)
track_count = length(status_info.tracks)
track_details = if track_count > 0 do
track_list = Enum.map_join(status_info.tracks, ", ", fn track ->
"#{track.type} (#{track.name})"
end)
"\n- **Active Tracks:** #{track_list}"
else
"\n- **Active Tracks:** None"
end
"""
## π WebRTC Ingress Monitor (Update ##{iteration})
**Last Update:** #{timestamp}
#{emoji} **Status:** #{status_text}
- **Ingress ID:** #{ingress.ingress_id}
- **WHIP URL:** `#{ingress.url}`
- **Room:** #{ingress.room_name}
- **Participant:** #{ingress.participant_identity}#{track_details}
### π WebRTC Connection Tips:
- Ensure your browser supports WebRTC
- Check that camera/microphone permissions are granted
- Verify network connectivity (WebRTC works through NAT/firewalls)
- Monitor browser console for WebRTC logs
### π§ Troubleshooting:
- If stuck on "Buffering": Check media source is active
- If "Error": Check browser console for detailed logs
- If "Inactive": Verify WHIP client is connecting to the URL
"""
else
"""
## β WebRTC Monitoring Error (Update ##{iteration})
**Last Update:** #{timestamp}
Failed to monitor ingress: #{inspect(status_info.error)}
"""
end
Kino.Frame.render(monitor_frame, Kino.Markdown.new(content))
Process.sleep(5000) # Wait 5 seconds
end)
# Final update
Kino.Frame.render(monitor_frame, Kino.Markdown.new("""
## π WebRTC Monitoring Complete
**Session finished.** Re-run this cell to start a new monitoring session.
"""))
else
Kino.Frame.render(monitor_frame, Kino.Markdown.new("""
## β οΈ WebRTC Monitoring Not Available
Please create a WHIP ingress first.
"""))
end
end)
monitor_frame
WebRTC Debugging and Diagnostics
IO.puts("π WebRTC Debugging and Diagnostics")
IO.puts("=" |> String.duplicate(50))
IO.puts("")
IO.puts("π **Browser Console Commands for WebRTC Debugging:**")
IO.puts("")
IO.puts("```javascript")
IO.puts("// Enable WebRTC logging in Chrome")
IO.puts("localStorage.setItem('debug', 'livekit*');")
IO.puts("")
IO.puts("// Check WebRTC connection stats")
IO.puts("pc.getStats().then(stats => {")
IO.puts(" stats.forEach(report => {")
IO.puts(" if (report.type === 'outbound-rtp') {")
IO.puts(" console.log('Outbound stats:', report);")
IO.puts(" }")
IO.puts(" });")
IO.puts("});")
IO.puts("")
IO.puts("// Monitor connection state changes")
IO.puts("pc.addEventListener('connectionstatechange', () => {")
IO.puts(" console.log('Connection state:', pc.connectionState);")
IO.puts("});")
IO.puts("")
IO.puts("// Monitor ICE connection state")
IO.puts("pc.addEventListener('iceconnectionstatechange', () => {")
IO.puts(" console.log('ICE connection state:', pc.iceConnectionState);")
IO.puts("});")
IO.puts("```")
IO.puts("")
IO.puts("π§ **Common WebRTC Issues and Solutions:**")
IO.puts("")
IO.puts("1οΈβ£ **WHIP Connection Fails**")
IO.puts(" β’ Check WHIP URL is accessible")
IO.puts(" β’ Verify CORS headers are set correctly")
IO.puts(" β’ Ensure HTTPS is used for secure contexts")
IO.puts(" β’ Check browser console for detailed errors")
IO.puts("")
IO.puts("2οΈβ£ **Media Access Denied**")
IO.puts(" β’ Ensure HTTPS for camera/microphone access")
IO.puts(" β’ Check browser permissions settings")
IO.puts(" β’ Try different browsers (Chrome/Firefox/Safari)")
IO.puts(" β’ Verify devices are not in use by other applications")
IO.puts("")
IO.puts("3οΈβ£ **Poor Video Quality**")
IO.puts(" β’ Adjust video constraints (resolution/framerate)")
IO.puts(" β’ Check available bandwidth")
IO.puts(" β’ Monitor WebRTC stats for packet loss")
IO.puts(" β’ Consider codec preferences (VP9 > VP8 > H.264)")
IO.puts("")
IO.puts("4οΈβ£ **Connection Timeouts**")
IO.puts(" β’ Check STUN/TURN server configuration")
IO.puts(" β’ Verify firewall/NAT settings")
IO.puts(" β’ Test with different networks")
IO.puts(" β’ Monitor ICE candidate gathering")
IO.puts("")
IO.puts("5οΈβ£ **Audio Issues**")
IO.puts(" β’ Check microphone permissions")
IO.puts(" β’ Verify audio constraints")
IO.puts(" β’ Test with different audio codecs")
IO.puts(" β’ Monitor audio levels in browser")
WebRTC vs RTMP Comparison
IO.puts("β‘ WebRTC (WHIP) vs RTMP Comparison")
IO.puts("=" |> String.duplicate(45))
IO.puts("")
comparison_data = [
{"Aspect", "WebRTC (WHIP)", "RTMP"},
{"---", "---", "---"},
{"Latency", "Sub-second (50-200ms)", "2-10 seconds"},
{"Protocol", "UDP-based WebRTC", "TCP-based RTMP"},
{"Firewall", "NAT/Firewall friendly", "May need port forwarding"},
{"Browser Support", "Native support", "Requires Flash/plugin"},
{"Encoding", "Hardware accelerated", "Software encoding"},
{"Adaptive", "Built-in adaptation", "Fixed bitrate"},
{"Complexity", "More complex setup", "Simpler configuration"},
{"Streaming Software", "Limited support", "Universal support (OBS)"},
{"Mobile", "Excellent", "Good with apps"},
{"Quality", "Dynamic quality", "Consistent quality"}
]
for [aspect, webrtc, rtmp] <- comparison_data do
IO.puts("| #{String.pad_trailing(aspect, 15)} | #{String.pad_trailing(webrtc, 20)} | #{rtmp}")
end
IO.puts("")
IO.puts("π **When to Choose WebRTC (WHIP):**")
IO.puts(" β
Ultra-low latency requirements")
IO.puts(" β
Browser-based streaming")
IO.puts(" β
Interactive applications")
IO.puts(" β
Mobile streaming apps")
IO.puts(" β
Firewall/NAT traversal needed")
IO.puts("")
IO.puts("π **When to Choose RTMP:**")
IO.puts(" β
Streaming software compatibility")
IO.puts(" β
Consistent video quality needed")
IO.puts(" β
Simpler setup requirements")
IO.puts(" β
Long-form content streaming")
IO.puts(" β
Established workflows")
Performance Optimization for WebRTC
# WebRTC optimization recommendations generator
optimization_form = Kino.Control.form(
[
connection_type: Kino.Input.select("Network Connection", [
{"High-speed WiFi/Ethernet", :high_speed},
{"Mobile 4G/5G", :mobile},
{"Limited bandwidth", :limited}
]),
device_type: Kino.Input.select("Streaming Device", [
{"Desktop/Laptop", :desktop},
{"Mobile phone", :mobile_device},
{"Tablet", :tablet}
]),
content_type: Kino.Input.select("Content Type", [
{"Live person (talking head)", :talking_head},
{"Screen sharing", :screen_share},
{"Gaming/high motion", :gaming}
])
],
submit: "Get Optimization Recommendations"
)
# Generate optimization recommendations
opt_params = Kino.Control.read(optimization_form)
IO.puts("π WebRTC Optimization Recommendations")
IO.puts("=" |> String.duplicate(50))
IO.puts("")
# Connection type optimizations
IO.puts("π **Network Optimization:**")
case opt_params.connection_type do
:high_speed ->
IO.puts(" β’ Use high resolution: 1920x1080")
IO.puts(" β’ High framerate: 30-60 fps")
IO.puts(" β’ Use VP9 codec for best quality")
IO.puts(" β’ Enable hardware acceleration")
:mobile ->
IO.puts(" β’ Moderate resolution: 1280x720")
IO.puts(" β’ Standard framerate: 30 fps")
IO.puts(" β’ Use adaptive bitrate")
IO.puts(" β’ Monitor data usage")
:limited ->
IO.puts(" β’ Lower resolution: 854x480")
IO.puts(" β’ Reduced framerate: 15-24 fps")
IO.puts(" β’ Use H.264 for efficiency")
IO.puts(" β’ Enable aggressive compression")
end
IO.puts("")
# Device type optimizations
IO.puts("π± **Device Optimization:**")
case opt_params.device_type do
:desktop ->
IO.puts(" β’ Use hardware encoding (NVENC/QuickSync)")
IO.puts(" β’ Multiple core utilization")
IO.puts(" β’ High-quality audio processing")
IO.puts(" β’ Multiple camera support")
:mobile_device ->
IO.puts(" β’ Enable low-power mode")
IO.puts(" β’ Use mobile-optimized codecs")
IO.puts(" β’ Monitor battery usage")
IO.puts(" β’ Thermal management")
:tablet ->
IO.puts(" β’ Balance quality and battery")
IO.puts(" β’ Optimize for tablet orientation")
IO.puts(" β’ Touch-friendly controls")
IO.puts(" β’ WiFi optimization")
end
IO.puts("")
# Content type optimizations
IO.puts("π₯ **Content Optimization:**")
case opt_params.content_type do
:talking_head ->
IO.puts(" β’ Focus on face detection")
IO.puts(" β’ Optimize for speech audio")
IO.puts(" β’ Moderate motion settings")
IO.puts(" β’ Good lighting important")
:screen_share ->
IO.puts(" β’ Optimize for text clarity")
IO.puts(" β’ Use screen-specific codecs")
IO.puts(" β’ Lower framerates acceptable")
IO.puts(" β’ Focus on specific regions")
:gaming ->
IO.puts(" β’ High framerate essential")
IO.puts(" β’ Low latency priority")
IO.puts(" β’ Disable post-processing")
IO.puts(" β’ Hardware encoding critical")
end
IO.puts("")
IO.puts("π‘ **Universal WebRTC Tips:**")
IO.puts(" β’ Test with multiple browsers")
IO.puts(" β’ Monitor WebRTC statistics")
IO.puts(" β’ Use TURN servers for NAT traversal")
IO.puts(" β’ Implement automatic quality adaptation")
IO.puts(" β’ Provide user feedback on connection quality")
Cleanup and Resource Management
# WebRTC ingress cleanup
cleanup_form = Kino.Control.form(
[
action: Kino.Input.select("Cleanup Action", [
{"List all WHIP ingress endpoints", :list_whip},
{"Delete current WHIP ingress", :delete_current},
{"Delete all test WHIP endpoints", :delete_test}
])
],
submit: "Execute Cleanup"
)
# Execute cleanup
cleanup_action = Kino.Control.read(cleanup_form)
client = Process.get(:client)
case cleanup_action.action do
:list_whip ->
case Livekit.IngressServiceClient.list_ingress(client) do
{:ok, response} ->
whip_ingress = Enum.filter(response.items, fn ingress ->
ingress.input_type == :WHIP_INPUT
end)
IO.puts("π WHIP Ingress Endpoints (#{length(whip_ingress)} total):")
for ingress <- whip_ingress do
{emoji, status_text} = WebRTCMonitor.format_status(ingress.state && ingress.state.status)
IO.puts(" #{emoji} #{ingress.name}")
IO.puts(" ID: #{ingress.ingress_id}")
IO.puts(" URL: #{ingress.url}")
IO.puts(" Room: #{ingress.room_name}")
IO.puts(" Status: #{status_text}")
IO.puts("")
end
{:error, reason} ->
IO.puts("β Failed to list WHIP ingress: #{inspect(reason)}")
end
:delete_current ->
ingress = Process.get(:whip_ingress)
if ingress do
request = %Livekit.DeleteIngressRequest{ingress_id: ingress.ingress_id}
case Livekit.IngressServiceClient.delete_ingress(client, request) do
{:ok, deleted} ->
IO.puts("β
Deleted current WHIP ingress: #{deleted.name}")
Process.delete(:whip_ingress)
{:error, reason} ->
IO.puts("β Failed to delete: #{inspect(reason)}")
end
else
IO.puts("β οΈ No current WHIP ingress to delete")
end
:delete_test ->
case Livekit.IngressServiceClient.list_ingress(client) do
{:ok, response} ->
test_whip = Enum.filter(response.items, fn ingress ->
ingress.input_type == :WHIP_INPUT &&
(String.contains?(ingress.name, "whip") ||
String.contains?(ingress.name, "test") ||
String.contains?(ingress.name, "demo"))
end)
if Enum.empty?(test_whip) do
IO.puts("β¨ No test WHIP ingress to clean up")
else
IO.puts("π§Ή Cleaning up #{length(test_whip)} test WHIP endpoints...")
for ingress <- test_whip do
request = %Livekit.DeleteIngressRequest{ingress_id: ingress.ingress_id}
case Livekit.IngressServiceClient.delete_ingress(client, request) do
{:ok, _} ->
IO.puts(" β
Deleted: #{ingress.name}")
{:error, reason} ->
IO.puts(" β Failed to delete #{ingress.name}: #{inspect(reason)}")
end
end
end
{:error, reason} ->
IO.puts("β Failed to cleanup: #{inspect(reason)}")
end
end
Summary and Next Steps
IO.puts("π WebRTC Ingress Tutorial Complete!")
IO.puts("=" |> String.duplicate(50))
IO.puts("")
IO.puts("β
**What You've Accomplished:**")
IO.puts(" β’ Created WebRTC (WHIP) ingress endpoints")
IO.puts(" β’ Generated browser-based streaming code")
IO.puts(" β’ Learned advanced WebRTC configurations")
IO.puts(" β’ Set up real-time performance monitoring")
IO.puts(" β’ Mastered WebRTC debugging techniques")
IO.puts(" β’ Optimized for different use cases")
IO.puts("")
IO.puts("π **Recommended Next Steps:**")
IO.puts(" 1. Test the generated HTML file in your browser")
IO.puts(" 2. Explore mobile WebRTC streaming applications")
IO.puts(" 3. Try the File Processing Livebook for batch ingestion")
IO.puts(" 4. Set up automated ingress management workflows")
IO.puts(" 5. Implement custom WebRTC applications")
IO.puts("")
IO.puts("π **Additional Resources:**")
IO.puts(" β’ WebRTC Spec: https://webrtc.org/")
IO.puts(" β’ WHIP Draft: https://datatracker.ietf.org/doc/draft-ietf-wish-whip/")
IO.puts(" β’ LiveKit WebRTC Guide: https://docs.livekit.io/")
IO.puts(" β’ Browser WebRTC APIs: https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API")