Eğitim için Video Yayını: WebRTC vs HLS vs DASH
Video, modern dijital eğitimin kalbidir. Bir üniversite dersinin canlı yayınından Coursera isteğe bağlı kütüphanesine, video görüşmesi yoluyla özel ders oturumundan kullanılan MOOC'a kadar Trende çevrimdışı olarak her senaryonun son derece farklı teknik ihtiyaçları vardır. Protokolü seçin yanlış, canlı oturumlarda dayanılmaz gecikmeler, öğrenciler için aşırı ara belleğe alma anlamına gelir sınırlı bant genişliğine sahip veya 100 eşzamanlı kullanıcıyı aşmayacak bir mimariye sahip.
2025'te eğitim platformları ek bir zorlukla karşı karşıya: izleyici kutuplaşması. Bir yanda, yüksek bant genişliğine sahip bağlamlarda kesintisiz 4K video bekleyen öğrenciler. Öte yandan UNESCO'ya göre hâlâ dünyadaki öğrencilerin yüzde 37'si dijital eğitime erişiyor Bağlantı hızı 1 Mbps'nin altında. Yalnızca ilk grup için tasarlanmış bir video mimarisi ikincisini sistematik olarak dışlar.
Bu makale üç baskın protokole derinlemesine bir bakış sunuyor: WebRTC, HLS e MPEG-DASH, gerçek eğitim senaryoları için, uygulamalarla Tüm kullanım durumlarını kapsayan hibrit bir mimari oluşturmak için beton ve desen.
Ne Öğreneceksiniz
- WebRTC'nin teknik mimarisi: ICE, STUN, TURN, SDP ve medya hattı
- HLS ve DASH: uyarlanabilir segmentasyon, bildirim dosyaları ve CDN dağıtımı
- Pratik karşılaştırma: gecikme süresi, ölçeklenebilirlik, cihaz desteği, DRM
- Hibrit mimari: Canlı yayın için WebRTC, VOD için HLS/DASH
- Sınırlı bant genişliği için optimizasyon: ABR, codec seçimi, ön yükleme
- React ve HLS.js ile eğitici bir oynatıcı uygulama
1. WebRTC: Canlı Dersler için Gerçek Zamanlı İletişim
WebRTC (Web Gerçek Zamanlı İletişim) ve iletişim için W3C standardı eklentiler olmadan tarayıcıda eşler arası ses/video. 200-500 ms gecikmelerle (6-30 saniyeye karşılık) HLS), WebRTC gerçek canlı etkileşimler için tek seçenektir: gerçek zamanlı Soru-Cevap içeren dersler, bire bir özel ders oturumları, etkileşimli sanal atölyeler.
WebRTC'nin karmaşıklığı sinyalizasyonda ve NAT geçişinde yatmaktadır. İki eş bağlantı kurmuyor doğrudan: öncelikle bir sinyal sunucusu aracılığıyla bağlantı bilgilerini alışverişinde bulunmaları gerekir, daha sonra ev ve kurumsal NAT'lar arasında geçiş yapmak için STUN/TURN'u kullanın.
// 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: İsteğe Bağlı İçerik Standardı
HTTP Canlı Akışı (HLS)Apple tarafından geliştirilen ve IETF tarafından standartlaştırılan, ve isteğe bağlı video ve geniş ölçekte canlı akış için baskın protokol. Onun gücü özel bir altyapı olmadan herhangi bir HTTP sunucusunda veya CDN'de çalıştırılmakta yatmaktadır. tüm cihazlarla evrensel uyumluluk veUyarlanabilir Bit Hızı Akışı (ABR) Bu da her bağlantı için mümkün olan en iyi kaliteyi garanti eder.
# 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: Açık Standart
MPEG-DASH (HTTP Üzerinden Dinamik Uyarlanabilir Akış) ve eşdeğer ISO standardı
HLS'ye ancak tamamen açık ve codec'ten bağımsız. DASH dosyaları kullanır .mpd (Medya Sunumu
Açıklama) posterler yerine .m3u8 HLS'den. HLS ve DASH arasındaki ana seçim
bugün ve esas olarak şunlar tarafından belirlenir:
| karakteristik | HLS | MPEG-DASH |
|---|---|---|
| Yerel Safari/iOS | Evet (yerli) | Hayır (dash.js gerektirir) |
| Yerel Chrome/Firefox | Hayır (hls.js gerektirir) | Kısmi (EME/MSE) |
| Codec desteği | H.264, H.265, VP9 | Herhangi biri (agnostik) |
| DRM (Geniş/Adil Oyun) | FairPlay (Apple), HLS+ aracılığıyla | Widevine, yerel PlayReady |
| Minimum segment süresi | 2s (önerilen 4-6s) | 1s (mümkünse alt saniye) |
| Canlı gecikme | LL-HLS: 2-3s, Standart: 6-30s | LL-DASH: 1-2s |
4. Eksiksiz Eğitim Teknolojisi Platformları için Hibrit Mimari
Eksiksiz bir eğitim platformu için en uygun çözüm, üç protokolü tek bir protokolde birleştirir otomatik geri dönüşlü hibrit mimari:
- İnteraktif canlı dersler (<50 öğrenci): Minimum gecikme için WebRTC mesh/SFU
- Canlı yayın dersleri (>50 öğrenci): Medya sunucusu aracılığıyla WebRTC yayınlama + HLS/DASH çıkışı
- VOD içeriği: CDN'de çoklu bit hızına sahip HLS (CloudFront, Fastly)
- Etkileşimle tekrar oynatma: Senkronize Soru-Cevap için HLS + WebSocket
# 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. Yavaş Bağlantılar İçin Optimizasyon
Kapsayıcı bir video mimarisi, sınırlı bağlantıya sahip öğrenciler için bile iyi çalışmalıdır. Bu optimizasyonlar küresel ölçeklenebilirlik açısından kritik öneme sahiptir:
// 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-Desen: iOS'ta Safari Gecikmesini Yoksay
iOS'taki Safari, HLS'yi yerel olarak destekler ancak bazı kritik davranışsal farklılıklara sahiptir:
MSE'yi (Medya Kaynağı Uzantıları) desteklemediğinden hls.js çalışmaz. Her zaman test etmek zorundasın
etiketiyle <video src="stream.m3u8"> iOS için yerel. Özellik algılamayı kullan
ile video.canPlayType('application/vnd.apple.mpegurl') hls.js'yi başlatmadan önce.
İPadOS'ta hls.js kitaplığı, iOS 17 sürümünden itibaren bazı sınırlamalarla çalışabilmektedir.
Özet: Hangi Protokolü Seçmeli
| Eğitim Teknolojisi senaryosu | Önerilen Protokol | Hedef Gecikme Süresi |
|---|---|---|
| İnteraktif 1:1 özel ders | WebRTC P2P | <300ms |
| Sanal sınıf <50 | WebRTC SFU (mediasoup, Janus) | <500ms |
| Canlı yayın dersi | LL-HLS veya WebRTC + HLS çıkışı | 2-5s |
| İsteğe bağlı video | Çoklu bit hızı HLS + CDN | Yok (VOD) |
| DRM korumalı içerik | DASH + Widevine/PlayReady | Yok (VOD) |
| Yavaş bağlantılar (<1Mbps) | Merdivenli HLS 360p + ABR | Yok (VOD) |
Sonuçlar
Eğitici video için tek bir "en iyi protokol" yoktur: her zaman doğru cevap "Senaryoya bağlı." Olgun bir Eğitim Teknolojisi mimarisi, canlı etkileşim için WebRTC'yi kullanır. Ölçeklenebilir dağıtım için HLS ve sürekliliği sağlayan otomatik geri dönüş sistemleri bozulmuş ağ koşullarında bile.
Video dağıtımına yapılan yatırım doğrudan kursun tamamlanma oranıyla ilgilidir: 2024 yılında 50.000 öğrenciyle yapılan bir çalışma, her saniyenin ek ara belleğe almanın tamamlanma oranını %5,8 azaltır. Video işlem hattınızı optimize edin ve öğrenmeyi optimize edin.
EdTech Serisindeki İlgili Makaleler
- Makale 00: Ölçeklenebilir LMS Mimarisi: Çok Kiracılı Desen
- Madde 07: CRDT ve WebSocket ile Gerçek Zamanlı İşbirliği
- Madde 08: Mobil Öncelikli Eğitim Teknolojisi: Çevrimdışı Öncelikli Mimari







