Streamování videa pro vzdělávání: WebRTC vs HLS vs DASH
Video je srdcem moderního digitálního vzdělávání. Z přímého přenosu vysokoškolské lekce do knihovny Coursera na vyžádání, od doučování přes videohovor až po použitý MOOC offline ve vlaku má každý scénář zcela odlišné technické potřeby. Vyberte protokol špatně znamená nesnesitelnou latenci v živých relacích, nadměrné ukládání do vyrovnávací paměti pro studenty s omezenou šířkou pásma nebo architekturou, která se neškáluje nad 100 souběžných uživatelů.
V roce 2025 čelí vzdělávací platformy další výzvě: polarizaci publika. Na jedné straně studenti v širokopásmových kontextech, kteří očekávají bezproblémové 4K video. Na druhou stranu, podle UNESCO má stále 37 % studentů na světě přístup k digitálnímu vzdělávání s konektivitou nižší než 1 Mbps. Video architektura navržená pouze pro první skupinu systematicky vylučuje druhou.
Tento článek se podrobně podívá na tři dominantní protokoly, WebRTC, HLS e MPEG-DASH, pro skutečné vzdělávací scénáře, s implementacemi beton a vzor k vytvoření hybridní architektury, která pokryje všechny případy použití.
Co se naučíte
- Technická architektura WebRTC: ICE, STUN, TURN, SDP a media pipeline
- HLS a DASH: adaptivní segmentace, soubory manifestů a distribuce CDN
- Praktické srovnání: latence, škálovatelnost, podpora zařízení, DRM
- Hybridní architektura: WebRTC pro živé vysílání, HLS/DASH pro VOD
- Optimalizace pro omezenou šířku pásma: ABR, výběr kodeku, předběžné načítání
- Implementace vzdělávacího přehrávače pomocí React a HLS.js
1. WebRTC: Komunikace v reálném čase pro živé lekce
WebRTC (webová komunikace v reálném čase) a standard W3C pro komunikaci peer-to-peer audio/video v prohlížeči bez pluginů. S latencí 200-500 ms (proti 6-30 sekundám HLS), WebRTC je jedinou volbou pro skutečné živé interakce: lekce s otázkami a odpověďmi v reálném čase, individuální doučování, interaktivní virtuální workshopy.
Složitost WebRTC spočívá v signalizaci a NAT traversal. Dva vrstevníci se nespojí přímo: nejprve si musí vyměňovat informace o připojení přes signalizační server, pak použijte STUN/TURN k procházení domácích a firemních NATů.
// Server WebRTC Signaling con Socket.io
// Gestisce lo scambio di SDP offer/answer e ICE candidates
import express from 'express';
import { createServer } from 'http';
import { Server as SocketServer } from 'socket.io';
interface Room {
hostSocketId: string;
participants: Set<string>;
maxParticipants: number;
}
const app = express();
const httpServer = createServer(app);
const io = new SocketServer(httpServer, {
cors: { origin: process.env.CORS_ORIGIN || '*' }
});
const rooms = new Map<string, Room>();
io.on('connection', (socket) => {
console.log(`Client connected: ${socket.id}`);
// --- JOIN ROOM ---
socket.on('join-room', (data: { roomId: string; role: 'host' | 'student' }) => {
const { roomId, role } = data;
if (!rooms.has(roomId)) {
if (role !== 'host') {
socket.emit('error', { message: 'Room not found' });
return;
}
rooms.set(roomId, {
hostSocketId: socket.id,
participants: new Set(),
maxParticipants: 200 // Limite per WebRTC server-side
});
}
const room = rooms.get(roomId)!;
if (room.participants.size >= room.maxParticipants) {
socket.emit('error', { message: 'Room full - join via HLS fallback' });
return;
}
socket.join(roomId);
room.participants.add(socket.id);
// Notifica host dell'ingresso nuovo studente
if (role === 'student') {
io.to(room.hostSocketId).emit('student-joined', {
studentId: socket.id,
participantCount: room.participants.size
});
}
socket.emit('room-joined', {
roomId,
hostSocketId: room.hostSocketId,
participantCount: room.participants.size
});
});
// --- WEBRTC SIGNALING ---
// Lo schema SFU (Selective Forwarding Unit) e preferito per classi > 4 persone
socket.on('offer', (data: { targetId: string; sdp: RTCSessionDescriptionInit }) => {
// Forwarda SDP offer al peer target
io.to(data.targetId).emit('offer', {
fromId: socket.id,
sdp: data.sdp
});
});
socket.on('answer', (data: { targetId: string; sdp: RTCSessionDescriptionInit }) => {
io.to(data.targetId).emit('answer', {
fromId: socket.id,
sdp: data.sdp
});
});
socket.on('ice-candidate', (data: { targetId: string; candidate: RTCIceCandidateInit }) => {
io.to(data.targetId).emit('ice-candidate', {
fromId: socket.id,
candidate: data.candidate
});
});
socket.on('disconnect', () => {
// Cleanup room se l'host disconnette
for (const [roomId, room] of rooms.entries()) {
room.participants.delete(socket.id);
if (room.hostSocketId === socket.id) {
io.to(roomId).emit('host-disconnected');
rooms.delete(roomId);
}
}
});
});
httpServer.listen(3001, () => console.log('Signaling server on :3001'));
// Client WebRTC - Classe per gestire la connessione dal lato studente
// Gestisce STUN/TURN, media capture e reconnection automatica
class EdTechWebRTCClient {
private peerConnection: RTCPeerConnection | null = null;
private localStream: MediaStream | null = null;
private remoteStream: MediaStream | null = null;
private readonly iceServers: RTCIceServer[] = [
// STUN pubblici Google (gratuiti)
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
// TURN server (necessario per NAT simmetrico ~15% degli utenti)
{
urls: 'turn:turn.youredtech.com:3478',
username: process.env.TURN_USERNAME!,
credential: process.env.TURN_CREDENTIAL!
}
];
constructor(
private signalingSocket: Socket,
private onRemoteStream: (stream: MediaStream) => void
) {
this.setupSignalingHandlers();
}
async joinAsStudent(roomId: string): Promise<void> {
// Crea PeerConnection
this.peerConnection = new RTCPeerConnection({
iceServers: this.iceServers,
iceTransportPolicy: 'all', // Usa 'relay' per forzare TURN in ambienti restrittivi
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require'
});
// Handler per stream remoto (video del docente)
this.peerConnection.ontrack = (event) => {
if (event.streams[0]) {
this.remoteStream = event.streams[0];
this.onRemoteStream(event.streams[0]);
}
};
// Handler per ICE candidates
this.peerConnection.onicecandidate = (event) => {
if (event.candidate) {
this.signalingSocket.emit('ice-candidate', {
targetId: 'host', // Semplificato - in prod usa l'ID reale
candidate: event.candidate.toJSON()
});
}
};
// Monitoraggio qualità connessione
this.peerConnection.onconnectionstatechange = () => {
const state = this.peerConnection?.connectionState;
console.log(`Connection state: ${state}`);
if (state === 'failed') {
this.handleConnectionFailure();
}
};
this.signalingSocket.emit('join-room', { roomId, role: 'student' });
}
private async handleConnectionFailure(): Promise<void> {
console.warn('WebRTC connection failed - attempting ICE restart');
try {
// ICE restart: rinegozia i candidati senza riavviare tutta la sessione
const offer = await this.peerConnection!.createOffer({ iceRestart: true });
await this.peerConnection!.setLocalDescription(offer);
this.signalingSocket.emit('offer', { sdp: offer });
} catch (err) {
// Fallback a HLS se WebRTC non recupera
console.error('ICE restart failed, switching to HLS fallback');
this.switchToHLSFallback();
}
}
private switchToHLSFallback(): void {
// Evento custom per far switchare il player a HLS
window.dispatchEvent(new CustomEvent('webrtc-fallback', {
detail: { hlsUrl: `${process.env.STREAM_CDN_URL}/live/stream.m3u8` }
}));
}
private setupSignalingHandlers(): void {
this.signalingSocket.on('offer', async (data: { sdp: RTCSessionDescriptionInit }) => {
await this.peerConnection!.setRemoteDescription(data.sdp);
const answer = await this.peerConnection!.createAnswer();
await this.peerConnection!.setLocalDescription(answer);
this.signalingSocket.emit('answer', { sdp: answer });
});
this.signalingSocket.on('ice-candidate', async (data: { candidate: RTCIceCandidateInit }) => {
await this.peerConnection!.addIceCandidate(data.candidate);
});
}
async getConnectionStats(): Promise<object> {
if (!this.peerConnection) return {};
const stats = await this.peerConnection.getStats();
const result: Record<string, unknown> = {};
stats.forEach((report) => {
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
result.packetsReceived = report.packetsReceived;
result.packetsLost = report.packetsLost;
result.jitter = report.jitter;
result.framesPerSecond = report.framesPerSecond;
result.bytesReceived = report.bytesReceived;
}
});
return result;
}
}
2. HLS: Standard pro obsah na vyžádání
HTTP Live Streaming (HLS)vyvinutý společností Apple a standardizovaný IETF, a dominantní protokol pro video na vyžádání a živé vysílání ve velkém měřítku. Jeho síla spočívá v běhu na jakémkoli HTTP serveru nebo CDN bez specializované infrastruktury, v univerzální kompatibilita se všemi zařízeními a vAdaptivní datový tok Streaming (ABR) což zaručuje nejlepší možnou kvalitu pro každé připojení.
# Pipeline FFmpeg per generare HLS multi-bitrate
# Crea 4 rappresentazioni (1080p, 720p, 480p, 360p) + master playlist
import subprocess
import os
from pathlib import Path
def create_hls_vod(
input_file: str,
output_dir: str,
segment_duration: int = 4
) -> str:
"""
Converte un video in formato HLS multi-bitrate per VOD educativo.
Restituisce il path del master manifest M3U8.
"""
Path(output_dir).mkdir(parents=True, exist_ok=True)
# Ladder di qualità per EdTech:
# 1080p per laboratori e presentazioni dettagliate
# 720p per lezioni standard
# 480p per connessioni mobili moderate
# 360p per connessioni lente (UNESCO: 37% studenti < 1Mbps)
quality_ladder = [
{'height': 1080, 'bitrate': 3000, 'maxrate': 3200, 'bufsize': 6000},
{'height': 720, 'bitrate': 1500, 'maxrate': 1600, 'bufsize': 3000},
{'height': 480, 'bitrate': 800, 'maxrate': 856, 'bufsize': 1600},
{'height': 360, 'bitrate': 400, 'maxrate': 428, 'bufsize': 800},
]
# Costruisce il comando FFmpeg
cmd = ['ffmpeg', '-i', input_file, '-preset', 'slow']
maps = []
var_stream_map = []
for i, q in enumerate(quality_ladder):
# Video stream per ogni qualità
cmd += [
'-map', '0:v',
f'-c:v:{i}', 'libx264',
f'-b:v:{i}', f'{q["bitrate"]}k',
f'-maxrate:v:{i}', f'{q["maxrate"]}k',
f'-bufsize:v:{i}', f'{q["bufsize"]}k',
f'-vf:v:{i}', f'scale=-2:{q["height"]}',
f'-profile:v:{i}', 'high',
]
var_stream_map.append(f'v:{i},a:{i}')
# Audio unificato
cmd += ['-map', '0:a']
for i in range(len(quality_ladder)):
cmd += [f'-c:a:{i}', 'aac', f'-b:a:{i}', '128k', f'-ac:{i}', '2']
# HLS output
cmd += [
'-f', 'hls',
'-hls_time', str(segment_duration),
'-hls_playlist_type', 'vod',
'-hls_flags', 'independent_segments',
'-hls_segment_type', 'mpegts',
'-hls_segment_filename', f'{output_dir}/stream_%v/segment_%03d.ts',
'-master_pl_name', 'master.m3u8',
'-var_stream_map', ' '.join(var_stream_map),
f'{output_dir}/stream_%v/playlist.m3u8'
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"FFmpeg failed: {result.stderr}")
# Aggiungi metadata educativi al master manifest
master_path = os.path.join(output_dir, 'master.m3u8')
_inject_metadata(master_path, quality_ladder)
return master_path
def _inject_metadata(manifest_path: str, quality_ladder: list) -> None:
"""
Inietta metadata nel manifest HLS per player educativo.
Aggiunge BANDWIDTH, RESOLUTION e FRAME-RATE.
"""
with open(manifest_path, 'r') as f:
content = f.read()
# Il master.m3u8 generato da FFmpeg e già corretto
# Qui potremmo aggiungere tag #EXT-X-SESSION-DATA per metadata del corso
enhanced = content.replace(
'#EXTM3U',
'#EXTM3U\n#EXT-X-SESSION-DATA:DATA-ID="course.chapter",VALUE="1"\n'
'#EXT-X-SESSION-DATA:DATA-ID="course.duration",VALUE="3600"'
)
with open(manifest_path, 'w') as f:
f.write(enhanced)
# Player React con HLS.js e Chapter Navigation
# Implementa feature specifiche per EdTech: capitoli, velocità, note
const EdTechHLSPlayer_CODE = `
import Hls from 'hls.js';
import { useEffect, useRef, useState, useCallback } from 'react';
interface Chapter {
time: number;
title: string;
thumbnail?: string;
}
interface PlayerProps {
src: string;
chapters?: Chapter[];
onProgress?: (time: number) => void;
onComplete?: () => void;
}
export function EdTechPlayer({ src, chapters = [], onProgress, onComplete }: PlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const hlsRef = useRef<Hls | null>(null);
const [currentLevel, setCurrentLevel] = useState(-1); // -1 = auto
const [isBuffering, setIsBuffering] = useState(false);
const [watchedSegments, setWatchedSegments] = useState<Set<number>>(new Set());
useEffect(() => {
const video = videoRef.current;
if (!video) return;
if (Hls.isSupported()) {
const hls = new Hls({
// Configurazione ottimizzata per EdTech
startLevel: -1, // Auto quality selection
capLevelToPlayerSize: true, // Non scaricare qualità > viewport
maxBufferLength: 60, // Buffer 60s per buffering predittivo
maxMaxBufferLength: 120,
lowLatencyMode: false, // Non necessario per VOD
progressive: true, // Inizia playback prima del download completo
// Timeout generosi per connessioni lente
manifestLoadingTimeOut: 15000,
levelLoadingTimeOut: 15000,
fragLoadingTimeOut: 30000,
// ABR aggressivo: scendi di qualità rapidamente su congestione
abrEwmaFastLive: 3.0,
abrEwmaSlowLive: 9.0,
abrBandWidthFactor: 0.8,
});
hls.loadSource(src);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play().catch(() => {}); // Gestisce autoplay policy
});
hls.on(Hls.Events.LEVEL_SWITCHED, (_, data) => {
setCurrentLevel(data.level);
});
hls.on(Hls.Events.BUFFER_STALLED_ERROR, () => setIsBuffering(true));
hls.on(Hls.Events.BUFFER_FLUSHED, () => setIsBuffering(false));
hlsRef.current = hls;
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari nativo HLS
video.src = src;
}
return () => hlsRef.current?.destroy();
}, [src]);
// Tracciamento progresso per analytics
const handleTimeUpdate = useCallback(() => {
const video = videoRef.current;
if (!video) return;
const currentTime = video.currentTime;
onProgress?.(currentTime);
// Marca segmento come visto (per completion tracking)
const segmentIndex = Math.floor(currentTime / 30); // Segmenti da 30s
setWatchedSegments(prev => new Set([...prev, segmentIndex]));
// Completion: considera completato a 90% del video
if (currentTime / video.duration > 0.9) {
onComplete?.();
}
}, [onProgress, onComplete]);
return (
<div className="edtech-player">
<video
ref={videoRef}
onTimeUpdate={handleTimeUpdate}
controls
playsInline
/>
{isBuffering && <div className="buffering-indicator">Buffering...</div>}
<div className="chapter-navigation">
{chapters.map((ch, i) => (
<button
key={i}
onClick={() => { if (videoRef.current) videoRef.current.currentTime = ch.time; }}
>
{ch.title}
</button>
))}
</div>
</div>
);
}
`;
3. MPEG-DASH: Otevřený standard
MPEG-DASH (Dynamic Adaptive Streaming over HTTP) a ekvivalentní norma ISO
na HLS, ale zcela otevřené a bez kodeků. DASH používá soubory .mpd (Mediální prezentace
Popis) místo plakátů .m3u8 společnosti HLS. Hlavní volba mezi HLS a DASH
dnes a hlavně určuje:
| Charakteristický | HLS | MPEG-DASH |
|---|---|---|
| Nativní Safari/iOS | Ano (nativní) | Ne (vyžaduje dash.js) |
| Nativní Chrome/Firefox | Ne (vyžaduje hls.js) | Částečná (EME/MSE) |
| Podpora kodeků | H.264, H.265, VP9 | Jakýkoli (agnostik) |
| DRM (Widevine/FairPlay) | FairPlay (Apple), přes HLS+ | Widevine, nativní PlayReady |
| Minimální trvání segmentu | 2 s (doporučeno 4-6 s) | 1 s (možná podsekunda) |
| Živá latence | LL-HLS: 2-3s, Standardní: 6-30s | LL-DASH: 1-2s |
4. Hybridní architektura pro kompletní platformy EdTech
Optimální řešení pro kompletní vzdělávací platformu kombinuje tři protokoly do jednoho hybridní architektura s automatickým návratem:
- Interaktivní živé lekce (<50 studentů): WebRTC mesh/SFU pro minimální latenci
- Lekce živého vysílání (>50 studentů): Publikování WebRTC + výstup HLS/DASH přes mediální server
- VOD obsahu: Multi-bitrate HLS na CDN (CloudFront, Fastly)
- Přehrajte si s interaktivitou: HLS + WebSocket pro synchronizované otázky a odpovědi
# Configurazione Nginx per Media Server ibrido
# Pattern: WebRTC -> RTMP -> HLS/DASH (via Nginx-RTMP module)
# nginx.conf
events {
worker_connections 4096;
}
http {
# Configurazione CORS per CDN distribution
map $http_origin $cors_origin {
default "";
~^https://.*\.youredtech\.com$ $http_origin;
}
server {
listen 8080;
# HLS endpoint
location /hls/ {
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
root /tmp/hls;
add_header Cache-Control no-cache;
add_header 'Access-Control-Allow-Origin' $cors_origin always;
# Chunk caching: ts segment lunghi 4s sono cacheable
location ~* \.ts$ {
add_header Cache-Control "public, max-age=60";
}
# M3U8 mai cacheable
location ~* \.m3u8$ {
add_header Cache-Control no-cache;
}
}
# Dash endpoint
location /dash/ {
root /tmp/dash;
add_header Cache-Control no-cache;
}
# Stats endpoint per monitoring
location /stat {
rtmp_stat all;
rtmp_stat_stylesheet stat.xsl;
}
}
}
rtmp {
server {
listen 1935;
chunk_size 4096;
application live {
live on;
record off;
# Genera HLS da stream RTMP
hls on;
hls_path /tmp/hls;
hls_fragment 4s;
hls_playlist_length 60s;
# Multi-bitrate con FFmpeg transcoding
exec ffmpeg -i rtmp://localhost/live/$name
-c:v libx264 -b:v 3000k -vf scale=-2:1080 -g 48 -sc_threshold 0
-f flv rtmp://localhost/hls/$name_1080p
-c:v libx264 -b:v 1500k -vf scale=-2:720 -g 48 -sc_threshold 0
-f flv rtmp://localhost/hls/$name_720p
-c:v libx264 -b:v 400k -vf scale=-2:360 -g 48 -sc_threshold 0
-f flv rtmp://localhost/hls/$name_360p;
}
# Applicazione per i diversi bitrate generati
application hls {
live on;
hls on;
hls_path /tmp/hls;
hls_fragment 4s;
hls_nested on;
# Genera master playlist
hls_variant _1080p BANDWIDTH=3000000,RESOLUTION=1920x1080;
hls_variant _720p BANDWIDTH=1500000,RESOLUTION=1280x720;
hls_variant _360p BANDWIDTH=400000,RESOLUTION=640x360;
}
}
}
5. Optimalizace pro pomalá připojení
Inkluzivní video architektura musí dobře fungovat i pro studenty s omezenou konektivitou. Tyto optimalizace jsou zásadní pro globální škálovatelnost:
// Strategia di preloading intelligente per EdTech
// Precarica la lezione successiva durante la visione di quella corrente
class SmartPreloader {
private readonly HLS_INSTANCE_MAP = new Map<string, Hls>();
private readonly PRELOAD_THRESHOLD = 0.7; // Inizia preload a 70% del video
constructor(private readonly curriculum: string[]) {}
setupPreloading(
currentVideoId: string,
currentHls: Hls,
video: HTMLVideoElement
): void {
const currentIndex = this.curriculum.indexOf(currentVideoId);
const nextVideoId = this.curriculum[currentIndex + 1];
if (!nextVideoId) return;
video.addEventListener('timeupdate', () => {
const progress = video.currentTime / video.duration;
if (progress > this.PRELOAD_THRESHOLD && !this.HLS_INSTANCE_MAP.has(nextVideoId)) {
this.preloadVideo(nextVideoId);
}
});
}
private preloadVideo(videoId: string): void {
// Usa preload: none inizialmente, poi passa a metadata
const preloadHls = new Hls({
startLevel: 0, // Inizia con qualità più bassa
maxBufferLength: 10, // Buffer minimo durante preload
maxMaxBufferLength: 30,
progressive: false,
});
const probeVideo = document.createElement('video');
probeVideo.preload = 'metadata';
preloadHls.loadSource(`/api/video/${videoId}/stream.m3u8`);
preloadHls.attachMedia(probeVideo);
// Precarica solo i primi segmenti
preloadHls.on(Hls.Events.FRAG_LOADED, (_, data) => {
if (data.frag.sn > 3) { // Stop dopo 3 segmenti (~12s)
preloadHls.stopLoad();
}
});
this.HLS_INSTANCE_MAP.set(videoId, preloadHls);
}
getPreloadedHls(videoId: string): Hls | undefined {
return this.HLS_INSTANCE_MAP.get(videoId);
}
}
// Network Quality Detection per adattamento automatico
async function detectNetworkQuality(): Promise<'high' | 'medium' | 'low'> {
// Usa Network Information API dove disponibile
const connection = (navigator as any).connection
|| (navigator as any).mozConnection
|| (navigator as any).webkitConnection;
if (connection) {
const downlink = connection.downlink; // Mbps
if (downlink >= 5) return 'high';
if (downlink >= 1) return 'medium';
return 'low';
}
// Fallback: misura bandwidth con probe request
const startTime = performance.now();
const PROBE_URL = '/api/bandwidth-probe?size=50000'; // 50KB probe
try {
const response = await fetch(PROBE_URL);
const buffer = await response.arrayBuffer();
const duration = (performance.now() - startTime) / 1000;
const sizeMB = buffer.byteLength / 1_000_000;
const speedMbps = (sizeMB * 8) / duration;
if (speedMbps >= 5) return 'high';
if (speedMbps >= 1) return 'medium';
return 'low';
} catch {
return 'medium'; // Default conservativo
}
}
Anti-Pattern: Ignorujte latenci Safari na iOS
Safari na iOS nativně podporuje HLS, ale s některými zásadními rozdíly v chování:
Nepodporuje MSE (Media Source Extensions), takže hls.js nefunguje. Vždy musíte testovat
se značkou <video src="stream.m3u8"> nativní pro iOS. Použijte detekci funkcí
s video.canPlayType('application/vnd.apple.mpegurl') před vytvořením instance hls.js.
Na iPadOS může knihovna hls.js fungovat s určitými omezeními od verze iOS 17.
Shrnutí: Jaký protokol zvolit
| Scénář EdTech | Doporučený protokol | Cílová latence |
|---|---|---|
| Interaktivní doučování 1:1 | WebRTC P2P | <300 ms |
| Virtuální třída <50 | WebRTC SFU (mediasoup, Janus) | <500 ms |
| Lekce živého vysílání | Výstup LL-HLS nebo WebRTC + HLS | 2-5s |
| Video na vyžádání | Vícebitová rychlost HLS + CDN | N/A (VOD) |
| obsah chráněný DRM | DASH + Widevine/PlayReady | N/A (VOD) |
| Pomalá připojení (<1Mbps) | HLS s žebříkem 360p + ABR | N/A (VOD) |
Závěry
Pro vzdělávací video neexistuje jediný „nejlepší protokol“: pokaždé správná odpověď "Záleží na scénáři." Vyspělá architektura EdTech využívá WebRTC pro živou interaktivitu, HLS pro škálovatelné nasazení a automatické záložní systémy, které zajišťují kontinuitu i ve zhoršených podmínkách sítě.
Investice do dodání videa přímo souvisí s mírou dokončení kurzu: studie z roku 2024 na 50 000 studentech ukázala, že každá sekunda dodatečného ukládání do vyrovnávací paměti snižuje míru dokončení o 5,8 %. Optimalizujte svůj kanál videí a optimalizujte učení.
Související články v sérii EdTech
- Článek 00: Škálovatelná architektura LMS: Vzor pro více nájemců
- Článek 07: Spolupráce s CRDT a WebSocket v reálném čase
- Článek 08: Mobile-First EdTech: Offline-First Architecture







