WHEP 拉流技术详解(基于一个 html/js demo)

WHEP 拉流技术详解

概述

WHEP (WebRTC-HTTP Egress Protocol) 是一个基于HTTP的WebRTC拉流协议,用于从媒体服务器拉取实时音视频流。本文档详细分析了WHEP拉流的实现原理、协议栈和代码实现。

项目结构

bash 复制代码
whep js demo/
├── whep_demo.html    # 前端WebRTC客户端
├── server.py         # Python HTTP服务器
└── WHEP拉流技术详解.md # 本文档
python 复制代码
#!/usr/bin/env python3
from http.server import HTTPServer, SimpleHTTPRequestHandler
import os

class Handler(SimpleHTTPRequestHandler):
    def end_headers(self):
        # 添加必需的COOP/COEP响应头以支持SharedArrayBuffer
        self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
        self.send_header("Cross-Origin-Opener-Policy", "same-origin")
        super().end_headers()
    
    def guess_type(self, path):
        # 重写MIME类型检测,确保WASM文件返回正确的MIME类型
        if path.endswith('.wasm'):
            return 'application/wasm'
        return super().guess_type(path)
    
    def do_GET(self):
        # 如果请求根路径,重定向到demo页面
        if self.path == '/':
            self.path = '/whep_demo.html'
        return super().do_GET()

if __name__ == "__main__":
    # 切换到dist目录
    os.chdir(os.path.join(os.path.dirname(__file__), '.'))
    
    print("🚀 启动FlightHub SDK测试服务器...")
    print("📁 服务目录:", os.getcwd())
    print("🌐 访问地址: http://localhost:8888")
    print("📄 演示页面: http://localhost:8888/whep_demo.html")
    print("⚠️  注意: 需要配置Chrome浏览器支持SharedArrayBuffer")
    print("🔧 Chrome配置: chrome://flags/#enable-experimental-web-platform-features")
    print("=" * 50)
    
    HTTPServer(("", 8888), Handler).serve_forever()
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WHEP 拉流演示</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        .container {
            background: white;
            border-radius: 15px;
            padding: 30px;
            box-shadow: 0 20px 40px rgba(0,0,0,0.1);
            max-width: 800px;
            width: 100%;
        }

        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 30px;
            font-size: 2.2em;
        }

        .video-container {
            position: relative;
            background: #000;
            border-radius: 10px;
            overflow: hidden;
            margin-bottom: 20px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.3);
        }

        #videoElement {
            width: 100%;
            height: 450px;
            object-fit: cover;
            display: block;
        }

        .controls {
            display: flex;
            gap: 15px;
            justify-content: center;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }

        button {
            background: linear-gradient(45deg, #667eea, #764ba2);
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 25px;
            cursor: pointer;
            font-size: 16px;
            font-weight: 600;
            transition: all 0.3s ease;
            box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
        }

        button:hover {
            transform: translateY(-2px);
            box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
        }

        button:disabled {
            background: #ccc;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }

        .status {
            text-align: center;
            padding: 15px;
            border-radius: 10px;
            margin-bottom: 20px;
            font-weight: 500;
        }

        .status.connecting {
            background: #fff3cd;
            color: #856404;
            border: 1px solid #ffeaa7;
        }

        .status.connected {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }

        .status.error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }

        .status.disconnected {
            background: #e2e3e5;
            color: #383d41;
            border: 1px solid #d6d8db;
        }

        .info {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 10px;
            border-left: 4px solid #667eea;
        }

        .info h3 {
            margin-top: 0;
            color: #333;
        }

        .info p {
            margin: 5px 0;
            color: #666;
        }

        .loading {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid #f3f3f3;
            border-top: 3px solid #667eea;
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin-right: 10px;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .hidden {
            display: none;
        }

        .url-input-group {
            margin: 10px 0;
        }

        .url-input-group label {
            display: block;
            margin-bottom: 5px;
            color: #333;
            font-weight: 600;
        }

        #whepUrlInput {
            width: 100%;
            padding: 12px 15px;
            border: 2px solid #e1e5e9;
            border-radius: 8px;
            font-size: 14px;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            transition: all 0.3s ease;
            box-sizing: border-box;
        }

        #whepUrlInput:focus {
            outline: none;
            border-color: #667eea;
            box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
        }

        #whepUrlInput::placeholder {
            color: #999;
            font-style: italic;
        }

        #whepUrlInput:disabled {
            background-color: #f8f9fa;
            color: #6c757d;
            cursor: not-allowed;
        }

        .url-presets {
            margin-top: 8px;
            display: flex;
            align-items: center;
            gap: 8px;
            flex-wrap: wrap;
        }

        .url-presets small {
            color: #666;
            font-weight: 500;
        }

        .preset-btn {
            background: #f8f9fa;
            color: #495057;
            border: 1px solid #dee2e6;
            padding: 4px 8px;
            border-radius: 4px;
            font-size: 12px;
            cursor: pointer;
            transition: all 0.2s ease;
        }

        .preset-btn:hover {
            background: #e9ecef;
            border-color: #adb5bd;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🎥 WHEP 拉流演示</h1>
        
        <div class="video-container">
            <video id="videoElement" autoplay muted playsinline></video>
        </div>

        <div id="status" class="status disconnected">
            准备就绪 - 点击开始拉流
        </div>

        <div class="controls">
            <button id="startBtn" onclick="startWhepStream()">
                ▶️ 开始拉流
            </button>
            <button id="stopBtn" onclick="stopWhepStream()" disabled>
                ⏹️ 停止拉流
            </button>
        </div>

        <div class="info">
            <h3>📡 流信息</h3>
            <div class="url-input-group">
                <label for="whepUrlInput"><strong>WHEP URL:</strong></label>
                <input type="text" id="whepUrlInput" 
                       value="http://10.62.66.15/rtc/v1/whep/?app=live&stream=livestream"
                       placeholder="请输入WHEP服务器地址,例如: http://server:port/rtc/v1/whep/?app=live&stream=livestream">
                <div class="url-presets">
                    <small>预设地址:</small>
                    <button type="button" class="preset-btn" onclick="setPresetUrl('http://10.62.66.15/rtc/v1/whep/?app=live&stream=livestream')">默认服务器</button>
                    <button type="button" class="preset-btn" onclick="setPresetUrl('http://localhost:8080/rtc/v1/whep/?app=live&stream=livestream')">本地服务器</button>
                </div>
            </div>
            <p><strong>协议:</strong> WebRTC-HTTP Egress Protocol (WHEP)</p>
            <p><strong>状态:</strong> <span id="connectionStatus">未连接</span></p>
            <p><strong>错误信息:</strong> <span id="errorInfo">无</span></p>
        </div>
    </div>

    <script>
        let pc = null;
        let isConnected = false;
        
        const videoElement = document.getElementById('videoElement');
        const statusDiv = document.getElementById('status');
        const startBtn = document.getElementById('startBtn');
        const stopBtn = document.getElementById('stopBtn');
        const connectionStatus = document.getElementById('connectionStatus');
        const errorInfo = document.getElementById('errorInfo');
        const whepUrlInput = document.getElementById('whepUrlInput');

        function getWhepUrl() {
            return whepUrlInput.value.trim();
        }

        function setPresetUrl(url) {
            whepUrlInput.value = url;
            whepUrlInput.focus();
        }

        function updateStatus(message, type = 'disconnected') {
            statusDiv.textContent = message;
            statusDiv.className = `status ${type}`;
            connectionStatus.textContent = type === 'connected' ? '已连接' : 
                                         type === 'connecting' ? '连接中...' : 
                                         type === 'error' ? '连接错误' : '未连接';
        }

        function updateError(error) {
            errorInfo.textContent = error || '无';
        }

        async function startWhepStream() {
            try {
                // 获取用户输入的WHEP URL
                const whepUrl = getWhepUrl();
                if (!whepUrl) {
                    updateStatus('❌ 请输入WHEP服务器地址', 'error');
                    updateError('WHEP URL不能为空');
                    return;
                }

                // 验证URL格式
                try {
                    new URL(whepUrl);
                } catch (e) {
                    updateStatus('❌ WHEP URL格式不正确', 'error');
                    updateError('请输入有效的URL地址');
                    return;
                }

                updateStatus('正在初始化连接...', 'connecting');
                updateError('');
                
                startBtn.disabled = true;
                stopBtn.disabled = false;
                whepUrlInput.disabled = true;

                // 创建RTCPeerConnection
                pc = new RTCPeerConnection({
                    iceServers: [
                        { urls: 'stun:stun.l.google.com:19302' },
                        { urls: 'stun:stun1.l.google.com:19302' }
                    ]
                });

                // 处理接收到的媒体流
                pc.ontrack = (event) => {
                    console.log('收到媒体轨道:', event.track);
                    videoElement.srcObject = event.streams[0];
                    updateStatus('✅ 拉流成功!正在播放视频', 'connected');
                };

                // 处理ICE连接状态变化
                pc.oniceconnectionstatechange = () => {
                    console.log('ICE连接状态:', pc.iceConnectionState);
                    if (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') {
                        updateStatus('✅ 连接已建立,正在接收视频流...', 'connected');
                    } else if (pc.iceConnectionState === 'failed') {
                        updateStatus('❌ ICE连接失败', 'error');
                        updateError('ICE连接失败,请检查网络连接');
                    }
                };

                // 处理连接状态变化
                pc.onconnectionstatechange = () => {
                    console.log('连接状态:', pc.connectionState);
                    if (pc.connectionState === 'connected') {
                        updateStatus('✅ 拉流成功!正在播放视频', 'connected');
                    } else if (pc.connectionState === 'failed') {
                        updateStatus('❌ 连接失败', 'error');
                        updateError('WebRTC连接失败');
                    }
                };

                // 创建offer
                const offer = await pc.createOffer({
                    offerToReceiveAudio: true,
                    offerToReceiveVideo: true
                });

                await pc.setLocalDescription(offer);

                console.log('发送WHEP请求到:', whepUrl);
                console.log('Offer SDP:', offer.sdp);

                // 发送WHEP请求
                const response = await fetch(whepUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/sdp',
                        'Accept': 'application/sdp'
                    },
                    body: offer.sdp
                });

                if (!response.ok) {
                    throw new Error(`WHEP请求失败: ${response.status} ${response.statusText}`);
                }

                const answerSdp = await response.text();
                console.log('收到Answer SDP:', answerSdp);

                // 设置远程描述
                const answer = new RTCSessionDescription({
                    type: 'answer',
                    sdp: answerSdp
                });

                await pc.setRemoteDescription(answer);
                console.log('远程描述设置完成');

                isConnected = true;

            } catch (error) {
                console.error('WHEP拉流错误:', error);
                updateStatus(`❌ 拉流失败: ${error.message}`, 'error');
                updateError(error.message);
                
                startBtn.disabled = false;
                stopBtn.disabled = true;
                whepUrlInput.disabled = false;
                
                if (pc) {
                    pc.close();
                    pc = null;
                }
            }
        }

        function stopWhepStream() {
            try {
                if (pc) {
                    pc.close();
                    pc = null;
                }
                
                if (videoElement.srcObject) {
                    videoElement.srcObject.getTracks().forEach(track => track.stop());
                    videoElement.srcObject = null;
                }

                updateStatus('已停止拉流', 'disconnected');
                updateError('');
                
                startBtn.disabled = false;
                stopBtn.disabled = true;
                whepUrlInput.disabled = false;
                isConnected = false;
                
                console.log('WHEP拉流已停止');
            } catch (error) {
                console.error('停止拉流时出错:', error);
                updateError(error.message);
            }
        }

        // 页面卸载时清理资源
        window.addEventListener('beforeunload', () => {
            if (pc) {
                pc.close();
            }
        });

        // 检查浏览器支持
        if (!window.RTCPeerConnection) {
            updateStatus('❌ 浏览器不支持WebRTC', 'error');
            updateError('您的浏览器不支持WebRTC,请使用现代浏览器');
            startBtn.disabled = true;
        }
    </script>
</body>
</html>

1. WHEP 拉流完整流程

1.1 初始化阶段

javascript 复制代码
// 创建RTCPeerConnection实例
pc = new RTCPeerConnection({
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        { urls: 'stun:stun1.l.google.com:19302' }
    ]
});

涉及协议:

  • WebRTC API: 浏览器原生WebRTC接口
  • STUN协议: NAT穿透和地址发现

1.2 媒体轨道处理设置

javascript 复制代码
// 设置接收媒体流的回调
pc.ontrack = (event) => {
    console.log('收到媒体轨道:', event.track);
    videoElement.srcObject = event.streams[0];
    updateStatus('✅ 拉流成功!正在播放视频', 'connected');
};

涉及协议:

  • WebRTC MediaStream API: 处理媒体轨道
  • HTML5 Video API: 视频播放

1.3 ICE连接状态监控

javascript 复制代码
// 监控ICE连接状态
pc.oniceconnectionstatechange = () => {
    console.log('ICE连接状态:', pc.iceConnectionState);
    if (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') {
        updateStatus('✅ 连接已建立,正在接收视频流...', 'connected');
    } else if (pc.iceConnectionState === 'failed') {
        updateStatus('❌ ICE连接失败', 'error');
        updateError('ICE连接失败,请检查网络连接');
    }
};

涉及协议:

  • ICE协议: Interactive Connectivity Establishment
  • STUN/TURN协议: NAT穿透和连接建立

1.4 SDP协商过程

javascript 复制代码
// 创建SDP Offer
const offer = await pc.createOffer({
    offerToReceiveAudio: true,
    offerToReceiveVideo: true
});

await pc.setLocalDescription(offer);

涉及协议:

  • SDP协议: Session Description Protocol
  • WebRTC Offer/Answer模型: 媒体能力协商

1.5 WHEP请求发送

javascript 复制代码
// 发送WHEP请求
const response = await fetch(whepUrl, {
    method: 'POST',
    headers: {
        'Content-Type': 'application/sdp',
        'Accept': 'application/sdp'
    },
    body: offer.sdp
});

涉及协议:

  • HTTP协议: 传输SDP信息
  • WHEP协议: WebRTC-HTTP Egress Protocol

2. 核心协议详解

2.1 WHEP协议 (WebRTC-HTTP Egress Protocol)

特点:

  • 基于HTTP的WebRTC拉流协议
  • 使用POST方法发送SDP Offer
  • 通过HTTP响应返回SDP Answer
  • 简化WebRTC信令交换过程

协议栈:

makefile 复制代码
应用层: WHEP (WebRTC-HTTP Egress Protocol)
传输层: HTTP/1.1
网络层: TCP/IP

2.2 SDP协议 (Session Description Protocol)

作用:

  • 描述媒体会话的格式和能力
  • 包含音频/视频编解码器信息
  • 指定网络传输参数

SDP Offer示例:

ini 复制代码
v=0
o=- 1234567890 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111
m=video 9 UDP/TLS/RTP/SAVPF 96

2.3 ICE协议 (Interactive Connectivity Establishment)

连接建立过程:

  1. 收集候选地址: 本地IP、STUN服务器返回的公网IP
  2. 交换候选地址: 通过SDP交换候选地址
  3. 连接检查: 测试候选地址对之间的连通性
  4. 选择最佳路径: 选择延迟最低的路径

2.4 STUN协议 (Session Traversal Utilities for NAT)

作用:

  • 发现NAT后的公网IP和端口
  • 检查NAT类型
  • 为TURN协议提供候选地址

消息流程:

vbscript 复制代码
客户端 → STUN服务器: Binding Request
STUN服务器 → 客户端: Binding Response (包含映射的公网IP:端口)

2.5 RTP/RTCP协议 (Real-time Transport Protocol)

RTP特点:

  • 传输实时音视频数据
  • 提供时间戳和序列号
  • 支持丢包检测和重传

RTCP作用:

  • 提供QoS反馈
  • 同步音视频流
  • 统计信息报告

3. 详细交互流程

阶段1: 初始化准备

scss 复制代码
客户端浏览器
    ↓
1. 检查WebRTC支持 (RTCPeerConnection)
    ↓
2. 创建RTCPeerConnection实例
    ↓
3. 配置ICE服务器 (STUN)
    ↓
4. 设置事件监听器 (ontrack, oniceconnectionstatechange)

阶段2: SDP协商 (WHEP协议)

scss 复制代码
客户端                    HTTP服务器                媒体服务器
    ↓                        ↓                        ↓
1. 创建Offer SDP
    ↓
2. 设置本地描述 (setLocalDescription)
    ↓
3. POST /rtc/v1/whep/     ← HTTP请求 (SDP Offer)
    ↓                        ↓
4. 转发SDP Offer          → 处理WHEP请求
    ↓                        ↓
5. 返回SDP Answer         ← 生成Answer SDP
    ↓
6. 设置远程描述 (setRemoteDescription)

阶段3: ICE连接建立

vbscript 复制代码
客户端                    STUN服务器              媒体服务器
    ↓                        ↓                        ↓
1. 收集ICE候选地址
    ↓
2. STUN Binding Request  → 获取公网IP:端口
    ↓
3. STUN Binding Response ← 返回映射地址
    ↓
4. 交换ICE候选地址 (通过SDP)
    ↓
5. ICE连接检查
    ↓
6. 建立P2P连接

阶段4: 媒体传输

css 复制代码
客户端                   媒体服务器
    ↓                        ↓
1. 接收媒体轨道 (ontrack事件)
    ↓
2. 绑定到video元素
    ↓
3. 开始播放视频
    ↓
4. RTP/RTCP数据流传输
    ↓
5. 实时视频播放

4. 协议栈层次结构

scss 复制代码
┌─────────────────────────────────────┐
│           应用层                    │
│  WHEP (WebRTC-HTTP Egress Protocol) │
├─────────────────────────────────────┤
│           表示层                    │
│     SDP (Session Description)       │
├─────────────────────────────────────┤
│           会话层                    │
│     WebRTC API (浏览器接口)         │
├─────────────────────────────────────┤
│           传输层                    │
│  HTTP/1.1, TCP, UDP, RTP/RTCP      │
├─────────────────────────────────────┤
│           网络层                    │
│           IP协议                    │
├─────────────────────────────────────┤
│           数据链路层                │
│        以太网/WiFi                  │
└─────────────────────────────────────┘

5. 代码实现分析

5.1 前端实现 (whep_demo.html)

主要功能:

  • WebRTC连接管理
  • 媒体流播放
  • 状态监控和错误处理
  • 用户界面交互
  • 动态WHEP URL配置 - 支持用户自定义拉流地址
  • URL格式验证 - 自动验证输入的URL格式
  • 预设地址快速选择 - 提供常用服务器地址快速选择

关键代码片段:

javascript 复制代码
// 动态获取WHEP URL
function getWhepUrl() {
    return whepUrlInput.value.trim();
}

// URL格式验证
const whepUrl = getWhepUrl();
if (!whepUrl) {
    updateStatus('❌ 请输入WHEP服务器地址', 'error');
    return;
}

try {
    new URL(whepUrl);
} catch (e) {
    updateStatus('❌ WHEP URL格式不正确', 'error');
    return;
}

// WHEP请求实现
const response = await fetch(whepUrl, {
    method: 'POST',
    headers: {
        'Content-Type': 'application/sdp',
        'Accept': 'application/sdp'
    },
    body: offer.sdp
});

// 处理Answer
const answerSdp = await response.text();
const answer = new RTCSessionDescription({
    type: 'answer',
    sdp: answerSdp
});
await pc.setRemoteDescription(answer);

新增功能:

javascript 复制代码
// 预设URL快速选择
function setPresetUrl(url) {
    whepUrlInput.value = url;
    whepUrlInput.focus();
}

// 连接时禁用输入框
whepUrlInput.disabled = true;

// 停止时重新启用输入框
whepUrlInput.disabled = false;

5.2 服务器实现 (server.py)

主要功能:

  • 静态文件服务
  • CORS安全头配置
  • WASM文件支持
  • 自动重定向

关键配置:

python 复制代码
class Handler(SimpleHTTPRequestHandler):
    def end_headers(self):
        # 添加必需的COOP/COEP响应头
        self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
        self.send_header("Cross-Origin-Opener-Policy", "same-origin")
        super().end_headers()
    
    def guess_type(self, path):
        # 确保WASM文件返回正确的MIME类型
        if path.endswith('.wasm'):
            return 'application/wasm'
        return super().guess_type(path)

6. 错误处理和状态管理

6.1 连接状态监控

javascript 复制代码
// ICE连接状态
pc.oniceconnectionstatechange = () => {
    console.log('ICE连接状态:', pc.iceConnectionState);
    // 处理不同连接状态
};

// 整体连接状态
pc.onconnectionstatechange = () => {
    console.log('连接状态:', pc.connectionState);
    // 处理连接成功/失败
};

6.2 错误处理机制

javascript 复制代码
try {
    // WHEP请求
    const response = await fetch(whepUrl, { ... });
    // 处理响应
} catch (error) {
    console.error('WHEP拉流错误:', error);
    updateStatus(`❌ 拉流失败: ${error.message}`, 'error');
    // 清理资源
    if (pc) {
        pc.close();
        pc = null;
    }
}

6.3 资源清理

javascript 复制代码
function stopWhepStream() {
    if (pc) {
        pc.close();  // 关闭WebRTC连接
        pc = null;
    }
    
    if (videoElement.srcObject) {
        videoElement.srcObject.getTracks().forEach(track => track.stop());
        videoElement.srcObject = null;  // 清理媒体流
    }
}

7. 使用场景和优势

7.1 主要应用场景

  • 实时直播观看: 低延迟视频传输
  • 实时监控系统: 安防监控、远程设备监控
  • 教育演示: WebRTC技术学习
  • 实时数据可视化: 实时图表和仪表板

7.2 技术优势

  • 低延迟: WebRTC提供毫秒级延迟 (100-300ms)
  • 高质量: 支持多种音视频编解码器
  • 跨平台: 基于Web标准,支持多浏览器
  • 易集成: 简单的HTTP API接口
  • 自适应: 支持自适应码率和网络状况

7.3 协议优势

  • HTTP信令: 简化WebRTC信令交换
  • ICE穿透: 自动处理NAT和防火墙
  • 媒体优化: RTP/RTCP提供实时传输优化
  • 错误恢复: 完整的错误处理和重连机制

8. 部署和配置

8.1 服务器配置

bash 复制代码
# 启动Python服务器
python3 server.py

# 服务器信息
端口: 8888
访问地址: http://localhost:8888
演示页面: http://localhost:8888/whep_demo.html

8.2 浏览器配置

Chrome浏览器配置:

vbnet 复制代码
chrome://flags/#enable-experimental-web-platform-features

必需的安全头:

makefile 复制代码
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

8.3 网络要求

  • STUN服务器: 用于NAT穿透
  • 媒体服务器: 提供WHEP接口
  • HTTPS支持: 现代浏览器要求安全连接

9. 总结

WHEP拉流技术是现代WebRTC应用的重要组成部分,通过HTTP协议简化信令交换,通过WebRTC提供高质量低延迟的媒体传输。这个演示项目展示了:

  1. 完整的WebRTC实现: 从连接建立到媒体播放
  2. 协议栈集成: HTTP、SDP、ICE、RTP等协议协同工作
  3. 错误处理机制: 完善的异常处理和资源管理
  4. 用户友好界面: 直观的状态显示和控制

该技术为实时音视频应用提供了可靠的技术基础,是学习WebRTC和实时通信技术的优秀示例。


文档版本 : 1.0
创建日期 : 2025年
相关文件: whep_demo.html, server.py

相关推荐
GetcharZp2 小时前
告别 Selenium!这款 Go 语言神器,让网页自动化与爬虫快到飞起!
后端
天下无贼2 小时前
【Python】2026版——FastAPI 框架快速搭建后端服务
后端·python·aigc
橙序员小站2 小时前
当所有人都在做 Agent,我想聊聊被遗忘的基础设施
后端·开源·aigc
小蚂蚁i2 小时前
LangChain 完全学习手册:看完就能上手
后端·python·ai编程
Memory_荒年2 小时前
TiDB:当 MySQL 遇上分布式,生了个“超级混血儿”
java·数据库·后端
一步一个脚印一个坑2 小时前
用 APM 全链路追踪,29ms 内定位到 Docker 部署的 SSL 配置错误
javascript·后端·监控
aircrushin2 小时前
端到端AI决策架构如何重塑实时协作体验?
前端·javascript·后端
苦瓜小生2 小时前
【黑马点评学习笔记 | 实战篇 】| 6-Redis消息队列
redis·笔记·后端
yhole3 小时前
springboot 修复 Spring Framework 特定条件下目录遍历漏洞(CVE-2024-38819)
spring boot·后端·spring