Strumieniowe przesyłanie wideo dla edukacji: WebRTC vs HLS vs DASH
Wideo jest sercem nowoczesnej edukacji cyfrowej. Z transmisji na żywo lekcji uniwersyteckiej do biblioteki na żądanie Coursera, od sesji korepetycji poprzez rozmowę wideo do wykorzystanego kursu MOOC offline w pociągu, każdy scenariusz ma zupełnie inne potrzeby techniczne. Wybierz protokół źle oznacza nieznośne opóźnienia w sesjach na żywo, nadmierne buforowanie dla studentów z ograniczoną przepustowością lub architekturą, która nie pozwala na skalowanie powyżej 100 jednoczesnych użytkowników.
W 2025 roku platformy edukacyjne stoją przed dodatkowym wyzwaniem: polaryzacją odbiorców. Z jednej strony studenci korzystający z dużej przepustowości, którzy oczekują płynnego wideo 4K. Z drugiej strony, według UNESCO, wciąż 37% uczniów na świecie ma dostęp do edukacji cyfrowej z łącznością mniejszą niż 1 Mb/s. Architektura wideo przeznaczona wyłącznie dla pierwszej grupy systematycznie wyklucza to drugie.
W tym artykule szczegółowo omówiono trzy dominujące protokoły: WebRTC, HLS e MPEG-DASH, dla rzeczywistych scenariuszy edukacyjnych, z wdrożeniami betonu i wzoru, aby zbudować architekturę hybrydową obejmującą wszystkie przypadki użycia.
Czego się nauczysz
- Architektura techniczna WebRTC: ICE, STUN, TURN, SDP i media rurociąg
- HLS i DASH: segmentacja adaptacyjna, pliki manifestów i dystrybucja CDN
- Praktyczne porównanie: opóźnienie, skalowalność, obsługa urządzeń, DRM
- Architektura hybrydowa: WebRTC dla transmisji na żywo, HLS/DASH dla VOD
- Optymalizacja dla ograniczonej przepustowości: ABR, wybór kodeka, wstępne ładowanie
- Implementacja odtwarzacza edukacyjnego z wykorzystaniem React i HLS.js
1. WebRTC: Komunikacja w czasie rzeczywistym podczas lekcji na żywo
WebRTC (komunikacja internetowa w czasie rzeczywistym) oraz standard komunikacji W3C audio/wideo peer-to-peer w przeglądarce bez wtyczek. Z opóźnieniami 200–500 ms (w porównaniu z 6–30 sekundami). HLS), WebRTC to jedyny wybór, jeśli chodzi o prawdziwą interakcję na żywo: lekcje z pytaniami i odpowiedziami w czasie rzeczywistym, indywidualne sesje korepetycji, interaktywne wirtualne warsztaty.
Złożoność WebRTC polega na sygnalizacji i przejściu NAT. Dwa urządzenia równorzędne nie łączą się bezpośrednio: muszą najpierw wymienić informacje o połączeniu za pośrednictwem serwera sygnalizacyjnego, następnie użyj STUN/TURN, aby przejść przez domowe i korporacyjne 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 treści na żądanie
Transmisja na żywo HTTP (HLS), opracowany przez Apple i ustandaryzowany przez IETF, oraz dominujący protokół wideo na żądanie i transmisji strumieniowej na żywo na dużą skalę. Jego siła polega na uruchomieniu na dowolnym serwerze HTTP lub CDN bez specjalistycznej infrastruktury, w uniwersalna kompatybilność ze wszystkimi urządzeniami i inPrzesyłanie strumieniowe z adaptacyjną szybkością transmisji (ABR) co gwarantuje najlepszą możliwą jakość każdego połączenia.
# 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: Otwarty standard
MPEG-DASH (dynamiczne przesyłanie strumieniowe adaptacyjne przez HTTP) i równoważną normę ISO
do HLS, ale całkowicie otwarty i niezależny od kodeków. DASH używa plików .mpd (Prezentacja medialna
Opis) zamiast plakatów .m3u8 z HLS. Główny wybór pomiędzy HLS i DASH
dzisiaj i determinowane głównie przez:
| Charakterystyczny | HLS | MPEG-DASH |
|---|---|---|
| Natywne Safari/iOS | Tak (natywny) | Nie (wymaga dash.js) |
| Natywna przeglądarka Chrome/Firefox | Nie (wymaga hls.js) | Częściowe (EME/MSE) |
| Obsługa kodeków | H.264, H.265, VP9 | Każdy (agnostyk) |
| DRM (Widevine/FairPlay) | FairPlay (Apple), poprzez HLS+ | Widevine, natywny PlayReady |
| Minimalny czas trwania segmentu | 2 s (zalecane 4-6 s) | 1s (możliwa sekunda) |
| Opóźnienie na żywo | LL-HLS: 2-3 s, Standard: 6-30 s | LL-DASH: 1-2 s |
4. Architektura hybrydowa dla kompletnych platform EdTech
Optymalne rozwiązanie dla kompletnej platformy edukacyjnej łączy trzy protokoły w jeden architektura hybrydowa z automatycznym powrotem:
- Interaktywne lekcje na żywo (<50 uczniów): Siatka WebRTC/SFU zapewniająca minimalne opóźnienia
- Lekcje transmitowane na żywo (>50 uczniów): publikacja WebRTC + wyjście HLS/DASH przez serwer multimediów
- treści VOD-owe: HLS o wielu bitrate na CDN (CloudFront, Fastly)
- Odtwórz ponownie z interaktywnością: HLS + WebSocket do zsynchronizowanych pytań i odpowiedzi
# 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. Optymalizacja dla wolnych połączeń
Integracyjna architektura wideo musi dobrze działać nawet w przypadku studentów z ograniczoną łącznością. Te optymalizacje mają kluczowe znaczenie dla globalnej skalowalności:
// 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: Ignoruj opóźnienie przeglądarki Safari na iOS
Safari na iOS obsługuje natywnie HLS, ale z pewnymi krytycznymi różnicami w zachowaniu:
Nie obsługuje MSE (Media Source Extensions), więc hls.js nie działa. Zawsze trzeba testować
z tagiem <video src="stream.m3u8"> natywny dla iOS. Użyj wykrywania funkcji
z video.canPlayType('application/vnd.apple.mpegurl') przed utworzeniem instancji hls.js.
W systemie iPadOS biblioteka hls.js może działać z pewnymi ograniczeniami od wersji iOS 17.
Podsumowanie: Który protokół wybrać
| Scenariusz EdTech | Zalecany protokół | Docelowe opóźnienie |
|---|---|---|
| Interaktywne korepetycje 1:1 | WebRTC P2P | <300 ms |
| Wirtualna klasa <50 | WebRTC SFU (zupa medialna, Janus) | <500ms |
| Lekcja transmitowana na żywo | Wyjście LL-HLS lub WebRTC + HLS | 2-5 s |
| Wideo na żądanie | Multibitrate HLS + CDN | Nie dotyczy (VOD) |
| Treść chroniona DRM | DASH + Widevine/PlayReady | Nie dotyczy (VOD) |
| Powolne połączenia (<1Mbps) | HLS z drabiną 360p + ABR | Nie dotyczy (VOD) |
Wnioski
Nie ma jednego „najlepszego protokołu” dla filmów edukacyjnych: za każdym razem prawidłowa odpowiedź „To zależy od scenariusza”. Dojrzała architektura EdTech wykorzystuje WebRTC do interakcji na żywo, HLS do skalowalnego wdrożenia i automatyczne systemy awaryjne zapewniające ciągłość nawet w złych warunkach sieciowych.
Inwestycja w dostawę wideo jest bezpośrednio powiązana ze wskaźnikiem ukończenia kursu: badanie przeprowadzone w 2024 r. na 50 000 studentów wykazało, że każda sekunda dodatkowego buforowania obniża wskaźnik ukończenia o 5,8%. Zoptymalizuj swój potok wideo i zoptymalizuj naukę.
Powiązane artykuły z serii EdTech
- Artykuł 00: Skalowalna architektura LMS: wzorzec wielu najemców
- Artykuł 07: Współpraca w czasie rzeczywistym z CRDT i WebSocket
- Artykuł 08: EdTech zorientowany na urządzenia mobilne: architektura zorientowana na tryb offline







