文章目录
- [WebRTC 本地视频文件推流 Demo 搭建指南 (Ubuntu 24.04)](#WebRTC 本地视频文件推流 Demo 搭建指南 (Ubuntu 24.04))
-
- 概述
- 系统架构
- 第1步:环境准备
- 第2步:安装依赖库
- [第3步:编译安装 libdatachannel](#第3步:编译安装 libdatachannel)
- 第4步:准备测试视频
- 第5步:创建项目
-
- [5.1 下载 cpp-httplib(HTTP 库)](#5.1 下载 cpp-httplib(HTTP 库))
- [5.2 创建 CMakeLists.txt](#5.2 创建 CMakeLists.txt)
- 5.3创建index.html
- [5.4 创建 main.cpp](#5.4 创建 main.cpp)
- 第6步:编译项目
- 第7步:运行测试
-
- [7.1 启动服务端](#7.1 启动服务端)
- [7.2 浏览器访问](#7.2 浏览器访问)
- [7.3 完整运行日志示例](#7.3 完整运行日志示例)
- 第8步:局域网其他设备访问配置
-
- [8.1 检查防火墙](#8.1 检查防火墙)
- [8.2 确认网络连通性](#8.2 确认网络连通性)
- [8.3 从手机/平板测试](#8.3 从手机/平板测试)
- 故障排查
-
- [Q1:浏览器一直显示"ICE: checking",连不上](#Q1:浏览器一直显示"ICE: checking",连不上)
- [Q2:页面加载后无响应,日志停在"Requesting offer..."](#Q2:页面加载后无响应,日志停在"Requesting offer...")
- Q3:收到视频流但黑屏/白屏
- [Q4:编译时提示 rtc::* API 不匹配](#Q4:编译时提示 rtc::* API 不匹配)
- Q5:想让视频循环播放
- Q6:想支持多个客户端同时观看
- [Q7:FFmpeg 子进程没有正常退出](#Q7:FFmpeg 子进程没有正常退出)
- 进阶扩展
-
- 添加音频支持
- [添加 STUN 服务器(跨网段)](#添加 STUN 服务器(跨网段))
- [Docker 部署](#Docker 部署)
- 总结
- 参考资源
WebRTC 本地视频文件推流 Demo 搭建指南 (Ubuntu 24.04)
概述
本指南将在 Ubuntu 24.04 上搭建一个基于 libdatachannel(C++ WebRTC 库)的本地视频文件推流系统:
- 服务端:C++ 程序,通过 FFmpeg 子进程将视频打包为 RTP 流,经 UDP 接收后转发到 WebRTC,内嵌 HTTP 信令服务
- 客户端:任意设备的浏览器,实时接收并播放视频流
- 信令方式:HTTP REST API 交换 SDP(集成在同一进程中)
- 网络要求:服务端与客户端在同一局域网,无需 STUN/TURN
最终效果:浏览器打开网页,自动建立连接并实时播放视频。
核心架构思路 :让 FFmpeg 负责 RTP 打包(而非手动使用 H264RtpPacketizer),C++ 通过 UDP socket 接收 RTP 包,修复 SSRC 后直接调用 track->send() 转发。这是参照 libdatachannel 官方 examples/media-sender 示例的做法,避免了手动分包与 libdatachannel 内部机制冲突导致的数据损坏。
系统架构
┌──────────────────────────────────────┐ ┌──────────────────────────┐
│ C++ 服务端 (推流端) Ubuntu 24.04 │ │ 浏览器 (播放端) │
│ │ │ 局域网内任意设备 │
│ ┌────────────────────────────┐ │ HTTP │ │
│ │ HTTP 服务器 (cpp-httplib) │ │◄─────►│ ┌────────────────────┐ │
│ │ - 提供 index.html │ │ 信令 │ │ index.html │ │
│ │ - POST /api/signaling │ │ │ │ + WebRTC API (JS) │ │
│ └──────────┬─────────────────┘ │ │ └────────┬───────────┘ │
│ │ │ │ │ │
│ ┌──────────▼─────────────────┐ │ SRTP │ ┌────────▼───────────┐ │
│ │ PeerConnection │ │◄─────►│ │ PeerConnection │ │
│ │ + Video Track (sendonly) │ │ 视频流│ │ + <video> 元素 │ │
│ │ + SSRC 修复 │ │ │ │ │ │
│ └──────────┬─────────────────┘ │ │ └────────────────────┘ │
│ │ │ │ │
│ ┌──────────▼─────────────────┐ │ │ │
│ │ UDP Socket (recvfrom) │ │ │ │
│ │ 接收 FFmpeg RTP 包 │ │ │ │
│ └──────────┬─────────────────┘ │ │ │
│ │ │ │ │
│ ┌──────────▼─────────────────┐ │ │ │
│ │ FFmpeg 子进程 │ │ │ │
│ │ ffmpeg -re -i video.mp4 │ │ │ │
│ │ -f rtp rtp://127.0.0.1:N │ │ │ │
│ └────────────────────────────┘ │ │ │
└──────────────────────────────────────┘ └──────────────────────────┘
信令流程(C++ 创建 Offer,浏览器创建 Answer):
浏览器 C++ 服务端
│ │
│ GET / (加载页面) │
│◄───────────────────────────────│
│ │
│ POST /api/signaling │
│ {type:"request"} │
│──────────────────────────────►│
│ │ 创建 PeerConnection
│ │ 添加 Video Track (H.264, sendonly)
│ │ 生成 Offer SDP
│ {type:"offer", sdp:"..."} │
│◄───────────────────────────────│
│ │
│ 设置 RemoteDescription (offer)│
│ 创建 Answer │
│ │
│ POST /api/signaling │
│ {type:"answer", sdp:"..."} │
│──────────────────────────────►│
│ │ 设置 RemoteDescription (answer)
│ │ 启动 FFmpeg → UDP → track->send()
│◄══════════ SRTP 视频流 ═══════│
│ │
│ <video> 实时播放 │
第1步:环境准备
bash
# 更新系统
sudo apt update && sudo apt upgrade -y
# 安装基础编译工具
sudo apt install -y build-essential cmake pkg-config git wget
第2步:安装依赖库
bash
# FFmpeg(运行时版本,用于推流时调用 ffmpeg 命令行)
sudo apt install -y ffmpeg
# OpenSSL(libdatachannel 依赖)
sudo apt install -y libssl-dev
# libsrtp2(libdatachannel 依赖,SRTP 加密)
sudo apt install -y libsrtp2-dev
# nlohmann-json(JSON 解析,header-only)
sudo apt install -y nlohmann-json3-dev
验证依赖:
bash
cmake --version # 应 >= 3.15
ffmpeg -version # 应 >= 4.4
dpkg -l | grep libssl-dev
dpkg -l | grep libsrtp2-dev
第3步:编译安装 libdatachannel
libdatachannel 是目前最成熟的 C++ WebRTC 开源实现,我们需要从源码编译。
bash
cd ~
git clone --depth 1 --branch v0.21.2 https://github.com/paullouisageneau/libdatachannel.git
cd libdatachannel
git submodule update --init --recursive
mkdir build && cd build
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DNO_MEDIA=OFF \
-DNO_EXAMPLES=ON
make -j$(nproc)
sudo make install
sudo ldconfig
验证安装:
bash
# 检查库文件
ls /usr/local/lib/libdatachannel*.so
# 检查头文件
ls /usr/local/include/rtc/
# 检查 pkg-config(如未找到需手动创建 .pc 文件,见故障排查)
pkg-config --modversion libdatachannel
注意 :如果
git submodule update很慢(GitHub 访问问题),可以在cmake时使用系统库:
bashcmake .. -DCMAKE_BUILD_TYPE=Release -DNO_MEDIA=OFF -DNO_EXAMPLES=ON \ -DUSE_SYSTEM_SRTP=ON -DUSE_SYSTEM_JSON=ON
GitHub 克隆慢的替代方案:可使用 ghproxy.net 镜像加速:
bashgit clone --depth 1 --branch v0.21.2 https://ghproxy.net/https://github.com/paullouisageneau/libdatachannel.git
第4步:准备测试视频
使用 FFmpeg 将视频转换为 WebRTC 兼容的 H.264 格式。
方案 A:使用你自己的视频文件
bash
ffmpeg -i your_input_video.mp4 \
-c:v libx264 \
-profile:v baseline \
-level 3.1 \
-pix_fmt yuv420p \
-an \
-vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2" \
~/test_video.mp4
方案 B:没有视频文件时,生成测试视频(彩色图形 + 时间戳)
bash
ffmpeg -f lavfi -i testsrc=duration=60:size=640x480:rate=30 \
-c:v libx264 -profile:v baseline -level 3.1 -pix_fmt yuv420p \
-an \
~/test_video.mp4
参数说明:
| 参数 | 含义 |
|---|---|
-profile:v baseline |
H.264 Baseline Profile,WebRTC 最兼容 |
-level 3.1 |
限制编码级别,确保浏览器支持 |
-pix_fmt yuv420p |
像素格式,浏览器普遍支持 |
-an |
去掉音频(本 Demo 仅推视频) |
scale=640:480 |
分辨率限制 |
第5步:创建项目
bash
mkdir -p ~/webrtc-streamer
cd ~/webrtc-streamer
项目需要 3 个文件(都放在同一目录):
webrtc-streamer/
├── CMakeLists.txt
├── main.cpp
└── httplib.h # 从 GitHub 下载(header-only)
5.1 下载 cpp-httplib(HTTP 库)
bash
cd ~/webrtc-streamer
wget https://raw.githubusercontent.com/yhirose/cpp-httplib/v0.16.3/httplib.h
# 如果 wget 被墙,使用镜像:
# wget https://ghproxy.net/https://raw.githubusercontent.com/yhirose/cpp-httplib/v0.16.3/httplib.h
5.2 创建 CMakeLists.txt
bash
cat > CMakeLists.txt << 'CMAKE_EOF'
cmake_minimum_required(VERSION 3.15) //CMAKE版本
project(webrtc_streamer VERSION 1.0 LANGUAGES CXX) //工程名 版本 语言
set(CMAKE_CXX_STANDARD 17) //C++17
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(PkgConfig REQUIRED) //依赖
pkg_check_modules(LIBDATACHANNEL REQUIRED libdatachannel)//lidatachannel
find_package(nlohmann_json REQUIRED)
find_package(Threads REQUIRED)
add_executable(webrtc_streamer main.cpp) //可执行文件
target_include_directories(webrtc_streamer PRIVATE //头文件搜索
${LIBDATACHANNEL_INCLUDE_DIRS}
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(webrtc_streamer PRIVATE //链接
${LIBDATACHANNEL_LIBRARIES}
nlohmann_json::nlohmann_json
Threads::Threads
)
CMAKE_EOF
5.3创建index.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebRTC 推流 Demo</title>
</head>
<body>
<h1>WebRTC 推流 Demo</h1>
<button id="btn" onclick="start()">开始播放</button>
<pre id="st" style="background:#111;color:#0f0;padding:10px;text-align:left;min-height:100px;"></pre>
<video id="v" autoplay playsinline muted style="max-width:95%;border:3px solid
red;background:black;min-width:400px;min-height:300px;"></video>
<script>
let pc = null;
let logLines = [];
function set(s) {
logLines.push(new Date().toLocaleTimeString() + ' ' + s);
if (logLines.length > 30) logLines.shift();
document.getElementById('st').textContent = logLines.join('\n');
console.log(s);
}
async function start() {
document.getElementById('btn').disabled = true;
set('连接中...');
pc = new RTCPeerConnection({ iceServers: [] });
pc.ontrack = function(e) {
set('ontrack: kind=' + e.track.kind + ' id=' + e.track.id + ' streams=' + e.streams.length);
var stream = e.streams[0];
if (!stream) {
set('ERROR: stream 为空!');
return;
}
set('stream id=' + stream.id + ' active=' + stream.active + ' tracks=' + stream.getTracks().length);
var vt = stream.getVideoTracks();
if (vt.length === 0) {
set('WARN: stream 没有视频轨');
} else {
set('视频轨: readyState=' + vt[0].readyState + ' muted=' + vt[0].muted);
vt[0].onunmute = function() { set('视频轨 unmuted'); };
vt[0].onended = function() { set('视频轨 ended'); };
}
var v = document.getElementById('v');
v.srcObject = stream;
set('srcObject 已设置');
v.onloadedmetadata = function() { set('metadata loaded: ' + v.videoWidth + 'x' + v.videoHeight); };
v.onplay = function() { set('video onplay'); };
v.onwaiting = function() { set('video onwaiting'); };
v.onerror = function() { set('video error: ' + (v.error ? v.error.message : 'unknown')); };
v.play().then(function() { set('play() resolved'); }).catch(function(err) { set('play() rejected: ' +
err.message); });
};
pc.oniceconnectionstatechange = function() {
set('ICE:' + pc.iceConnectionState);
};
pc.onconnectionstatechange = function() {
set('Conn:' + pc.connectionState);
};
try {
set('获取Offer...');
var r1 = await fetch('/api/offer');
var offer = await r1.json();
set('Offer收到, len=' + offer.sdp.length);
// 解析 Offer SDP 中的 video 行
var offerLines = offer.sdp.split('\n');
for (var i = 0; i < offerLines.length; i++) {
if (offerLines[i].indexOf('m=video') === 0 || offerLines[i].indexOf('a=rtpmap') === 0 ||
offerLines[i].indexOf('a=fmtp') === 0) {
set('Offer: ' + offerLines[i]);
}
}
await pc.setRemoteDescription(new RTCSessionDescription(offer));
set('远端Offer已设置');
var answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
var answerLines = answer.sdp.split('\n');
for (var i = 0; i < answerLines.length; i++) {
if (answerLines[i].indexOf('m=video') === 0 || answerLines[i].indexOf('a=rtpmap') === 0 ||
answerLines[i].indexOf('a=fmtp') === 0) {
set('Answer: ' + answerLines[i]);
}
}
await new Promise(function(resolve) {
if (pc.iceGatheringState === 'complete') { resolve(); return; }
var h = function() {
if (pc.iceGatheringState === 'complete') {
pc.removeEventListener('icegatheringstatechange', h);
resolve();
}
};
pc.addEventListener('icegatheringstatechange', h);
setTimeout(function() {
pc.removeEventListener('icegatheringstatechange', h);
resolve();
}, 5000);
});
set('发送Answer...');
var r2 = await fetch('/api/answer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'answer', sdp: pc.localDescription.sdp })
});
set('Answer已发送, status=' + r2.status);
set('等待连接...');
} catch(err) {
set('ERROR: ' + err.message);
console.error(err);
document.getElementById('btn').disabled = false;
}
}
</script>
</body>
</html>
5.4 创建 main.cpp
c++
#include <sys/wait.h>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <csignal>
#include <cstring>
#include <fstream>
#include <iostream>
#include <memory>
#include <mutex>
#include <sstream>
#include <thread>
#include <rtc/rtc.hpp>
#include "httplib.h"
#include <nlohmann/json.hpp>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <signal.h>
using json = nlohmann::json;
std::atomic<bool> g_running{true};
std::unique_ptr<httplib::Server> g_httpServer;
std::shared_ptr<rtc::PeerConnection> g_pc;
std::shared_ptr<rtc::Track> g_videoTrack;
const rtc::SSRC g_ssrc = 42;
std::thread g_streamThread;
pid_t g_ffmpegPid = 0;
std::mutex g_signalingMutex;
std::condition_variable g_signalingCv;
std::string g_offerSdp;
bool g_offerReady = false;
static std::string g_html;
static std::string g_videoPath;
static bool loadHtmlFile() {
std::ifstream f("index.html");
if (!f.is_open()) { std::cerr << "[!] index.html missing" << std::endl; return false; }
std::stringstream ss; ss << f.rdbuf(); g_html = ss.str();
return true;
}
static void streamingThread(int udpPort) {
std::cout << "[stream] waiting..." << std::endl;
{
std::unique_lock<std::mutex> lk(g_signalingMutex);
g_signalingCv.wait(lk, [] { return g_offerReady || !g_running; });
}
if (!g_running) return;
// 启动 FFmpeg(后台,RTP 输出到 UDP)
std::string cmd = "ffmpeg -re -stream_loop -1 -i \"" + g_videoPath +
"\" -an -c:v libx264 -profile:v baseline -level 3.1 -tune zerolatency -f rtp rtp://127.0.0.1:" +
std::to_string(udpPort) + " > /tmp/ffmpeg.log 2>&1 & echo $!";
std::cout << "[stream] cmd: " << cmd << std::endl;
FILE* fp = popen(cmd.c_str(), "r");
if (fp) {
char buf[32];
if (fgets(buf, sizeof(buf), fp)) g_ffmpegPid = (pid_t)std::stoi(buf);
pclose(fp);
std::cout << "[stream] ffmpeg pid=" << g_ffmpegPid << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
// 创建 UDP socket
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) { std::cerr << "[!] socket" << std::endl; return; }
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(udpPort);
if (bind(sock, (const sockaddr*)&addr, sizeof(addr)) < 0) {
std::cerr << "[!] bind port " << udpPort << std::endl;
close(sock); return;
}
int rcvBuf = 212992;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvBuf, sizeof(rcvBuf));
std::cout << "[stream] listening UDP :" << udpPort << std::endl;
std::vector<uint8_t> buf(4096);
int n = 0;
while (g_running) {
ssize_t len = recv(sock, buf.data(), buf.size(), 0);
if (len < 0) {
if (errno == EINTR) continue;
break;
}
if (len < (ssize_t)sizeof(rtc::RtpHeader)) continue;
if (!g_videoTrack || !g_videoTrack->isOpen()) continue;
auto rtp = reinterpret_cast<rtc::RtpHeader*>(buf.data());
rtp->setSsrc(g_ssrc);
g_videoTrack->send(
reinterpret_cast<const std::byte*>(buf.data()), (size_t)len);
n++;
if (n % 900 == 0)
std::cout << "[stream] " << n << " pkts" << std::endl;
}
close(sock);
std::cout << "[stream] done " << n << " pkts" << std::endl;
}
static std::string getLocalSdp() {
auto o = g_pc->localDescription();
return o.has_value() ? std::string(o.value()) : "";
}
static bool createOffer() {
try {
g_pc = std::make_shared<rtc::PeerConnection>(rtc::Configuration{});
g_pc->onStateChange([](auto s) {
std::cout << "[RTC] state=" << (int)s << std::endl; });
g_pc->onGatheringStateChange([](auto s) {
std::cout << "[RTC] ice=" << (int)s << std::endl; });
// 完全参照官方 media-sender 示例
rtc::Description::Video media("video",
rtc::Description::Direction::SendOnly);
media.addH264Codec(96);
media.addSSRC(g_ssrc, "video-send");
g_videoTrack = g_pc->addTrack(media);
// 不创建 H264RtpPacketizer!不设 setMediaHandler!
// FFmpeg 负责 RTP 打包,这里只转发
g_pc->setLocalDescription();
std::mutex m; std::condition_variable c; bool done = false;
g_pc->onGatheringStateChange([&](auto s) {
if (s == rtc::PeerConnection::GatheringState::Complete) {
std::lock_guard<std::mutex> lk(m);
g_offerSdp = getLocalSdp(); done = true; c.notify_one();
}
});
{
std::unique_lock<std::mutex> lk(m);
if (!c.wait_for(lk, std::chrono::seconds(15), [&] { return done; }))
g_offerSdp = getLocalSdp();
}
std::cout << "[RTC] Offer ready len=" << g_offerSdp.size() << std::endl;
return true;
} catch (const std::exception& e) {
std::cerr << "[RTC] " << e.what() << std::endl; return false;
}
}
static void startHttpServer(int port) {
g_httpServer = std::make_unique<httplib::Server>();
g_httpServer->Get("/", [](auto&, auto& res) {
res.set_content(g_html, "text/html; charset=utf-8");
});
g_httpServer->Get("/api/offer", [](auto&, auto& res) {
if (g_pc) { g_pc->close(); g_pc.reset(); }
if (!createOffer()) {
res.status = 500;
res.set_content(R"({"error":"offer"})", "application/json");
return;
}
res.set_content(
json{{"type","offer"},{"sdp",g_offerSdp}}.dump(),
"application/json");
});
g_httpServer->Post("/api/answer", [](auto& req, auto& res) {
try {
json j = json::parse(req.body);
std::string sdp = j.value("sdp", "");
if (sdp.empty()) { res.status = 400; return; }
g_pc->setRemoteDescription(rtc::Description(sdp, "answer"));
g_pc->onStateChange([](auto s) {
if (s == rtc::PeerConnection::State::Connected) {
std::lock_guard<std::mutex> lk(g_signalingMutex);
if (!g_offerReady) {
g_offerReady = true;
g_signalingCv.notify_all();
std::cout << "[RTC] connected!" << std::endl;
}
}
});
res.set_content(R"({"status":"ok"})", "application/json");
} catch (const std::exception& e) {
res.status = 500;
res.set_content(json{{"error",e.what()}}.dump(), "application/json");
}
});
std::cout << "\n>>> http://0.0.0.0:" << port << " <<<\n" << std::endl;
g_httpServer->listen("0.0.0.0", port);
}
static void cleanup() {
g_running = false;
g_signalingCv.notify_all();
if (g_ffmpegPid > 0) {
kill(g_ffmpegPid, SIGTERM);
int waitCount = 0;
while (waitpid(g_ffmpegPid, nullptr, WNOHANG) == 0 && waitCount < 30) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
++waitCount;
}
if (waitpid(g_ffmpegPid, nullptr, WNOHANG) == 0) {
kill(g_ffmpegPid, SIGKILL);
waitpid(g_ffmpegPid, nullptr, 0);
}
}
if (g_streamThread.joinable()) g_streamThread.join();
if (g_pc) g_pc->close();
std::cout << "Program exited" << std::endl;
}
static void sigHandler(int) { cleanup(); }
int main(int argc, char* argv[]) {
g_videoPath = "test_video.mp4";
int port = 8080;
int udpPort = 6000;
if (argc >= 2) g_videoPath = argv[1];
if (argc >= 3) port = std::stoi(argv[2]);
if (argc >= 4) udpPort = std::stoi(argv[3]);
std::cout << "=== WebRTC Streamer ===\n"
<< "video: " << g_videoPath << "\n"
<< "http : " << port << "\n"
<< "udp : " << udpPort << std::endl;
signal(SIGINT, sigHandler);
signal(SIGTERM, sigHandler);
if (!loadHtmlFile()) return 1;
rtc::InitLogger(rtc::LogLevel::Warning);
g_streamThread = std::thread(streamingThread, udpPort);
startHttpServer(port);
cleanup();
if (g_streamThread.joinable()) g_streamThread.join();
if (g_pc) g_pc->close();
return 0;
}
第6步:编译项目
bash
cd ~/webrtc-streamer
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
编译成功后,build 目录下会生成 webrtc_streamer 可执行文件。
常见编译错误
错误1 :fatal error: nlohmann/json.hpp: No such file or directory
bash
# 手动下载 header-only 版本到项目目录
cd ~/webrtc-streamer
wget https://github.com/nlohmann/json/releases/download/v3.11.3/json.hpp
# 然后修改 main.cpp 中的 #include:
# 将 #include <nlohmann/json.hpp> 改为 #include "json.hpp"
同时也修改 CMakeLists.txt,去掉 find_package(nlohmann_json REQUIRED) 和 nlohmann_json::nlohmann_json 这两行。
错误2 :undefined reference to rtc::*
说明 libdatachannel 没有正确安装。重新执行第3步,确认 sudo ldconfig 已执行。
错误3 :fatal error: rtc/rtc.hpp: No such file or directory
bash
# 确认头文件位置
ls /usr/local/include/rtc/
# 如果不存在,重新执行 sudo make install
错误4 :pkg-config: No package 'libdatachannel' found
手动创建 pkg-config 文件:
bash
sudo tee /usr/local/lib/pkgconfig/libdatachannel.pc > /dev/null << 'EOF'
prefix=/usr/local
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: libdatachannel
Description: WebRTC Data Channels and Media Transport
Version: 0.21.2
Libs: -L${libdir} -ldatachannel
Cflags: -I${includedir}
EOF
第7步:运行测试
7.1 启动服务端
bash
cd ~/webrtc-streamer/build
# 将测试视频复制到 build 目录
cp ~/test_video.mp4 .
# 启动(默认使用 test_video.mp4 + HTTP 端口 8080, UDP 端口 9080)
./webrtc_streamer
# 或指定视频文件和端口
./webrtc_streamer /home/yourname/myvideo.mp4 9090
启动后输出类似:
=== WebRTC Video Streaming Demo ===
Video file: test_video.mp4
HTTP port: 8080
UDP port: 9080
========================================
HTTP server started: http://0.0.0.0:8080
========================================
[stream] Waiting for browser answer...
[WebRTC] Generated SSRC: 268435456 (0x10000000)
...
7.2 浏览器访问
-
在 Ubuntu 本机测试 :打开浏览器访问
http://localhost:8080- 页面会自动发起连接,无需点击按钮
- 几秒后即可看到视频
-
从局域网其他设备访问:先查看 Ubuntu 的 IP:
bashhostname -I # 或 ip addr show | grep "inet " | grep -v 127.0.0.1然后在其他设备的浏览器输入
http://<IP>:8080,例如http://192.168.1.100:8080
7.3 完整运行日志示例
=== WebRTC Video Streaming Demo ===
Video file: test_video.mp4
HTTP port: 8080
UDP port: 9080
========================================
HTTP server started: http://0.0.0.0:8080
========================================
[stream] Waiting for browser answer...
[signaling] Browser requested offer
[WebRTC] Generated SSRC: 270123456 (0x101a2c40)
[WebRTC] Offer created, SDP length: 952 bytes
[signaling] Offer sent to browser
[signaling] Received answer, SDP length: 784 bytes
[signaling] RemoteDescription set (answer)
[WebRTC] ICE gathering state: 2
[signaling] Answer processed, streaming will start
[stream] Starting FFmpeg: ffmpeg -re -stream_loop -1 -i test_video.mp4 ...
[stream] UDP socket bound to port 9080, waiting for RTP packets...
[WebRTC] Connection state: 3
[stream] Forwarded 300 RTP packets
[stream] Forwarded 600 RTP packets
第8步:局域网其他设备访问配置
8.1 检查防火墙
Ubuntu 24.04 默认防火墙通常未启用,但如果有:
bash
# 检查防火墙状态
sudo ufw status
# 如果启用了,开放 HTTP 端口
sudo ufw allow 8080/tcp
# WebRTC 媒体数据走 UDP 随机端口,测试时可临时关闭防火墙
sudo ufw disable # 仅测试用
8.2 确认网络连通性
从客户端设备 ping Ubuntu 服务器:
bash
ping 192.168.1.100 # 替换为实际 IP
8.3 从手机/平板测试
在同一 WiFi 下,手机浏览器访问 http://<Ubuntu_IP>:8080,页面自动连接并播放。
故障排查
Q1:浏览器一直显示"ICE: checking",连不上
原因:WebRTC 的 ICE 连接无法建立。
排查:
-
确保浏览器和服务器在同一局域网同一网段(如都是 192.168.1.x)
-
检查是否有防火墙阻止 UDP 端口:
bashsudo iptables -L -n | grep -i drop -
尝试在 Ubuntu 上临时关闭防火墙:
bashsudo ufw disable
Q2:页面加载后无响应,日志停在"Requesting offer..."
原因:信令 API 未响应。
排查:
bash
# 检查服务端是否在运行
ss -tlnp | grep 8080
# 手动测试信令 API
curl -X POST http://localhost:8080/api/signaling \
-H "Content-Type: application/json" \
-d '{"type":"request"}'
Q3:收到视频流但黑屏/白屏
原因:通常是 FFmpeg RTP 推流有问题,或者 H.264 编码格式不兼容。
排查步骤:
-
首先检查 FFmpeg 日志(这是最重要的第一步):
bashcat /tmp/ffmpeg.log- 如果文件为空 → FFmpeg 没有启动或静默失败了
- 如果有
Codec not found或Invalid data→ 源视频编码不兼容(见下方解决方案) - 如果有
speed=等信息 → FFmpeg 正常运行,问题在 RTP 转发链路
-
确认源视频编码:
bashffprobe -v error -select_streams v:0 -show_entries stream=codec_name,profile -of csv=p=0 test_video.mp4输出应为
h264,Constrained Baseline或h264,Baseline。 -
重新编码视频(如果编码不对):
bashffmpeg -i your_video.mp4 -c:v libx264 -profile:v baseline -level 3.1 \ -pix_fmt yuv420p -an -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2" \ ~/test_video.mp4 -
如果日志正常但仍黑屏 ,检查 UDP socket 是否收到数据:
在 main.cpp 的 recvfrom 后加一行调试日志(临时):
cppstd::cout << "RTP packet: " << len << " bytes, seq=" << ntohs(*(uint16_t*)(buffer+2)) << std::endl;确认有 RTP 包在接收。
Q4:编译时提示 rtc:😗 API 不匹配
原因:libdatachannel 版本不同,API 可能有变化。
解决:
-
确认安装的是 v0.21.2 版本
-
查看实际安装的头文件:
bashgrep -r "addH264Codec\|addSSRC\|setMediaHandler" /usr/local/include/rtc/ -
如果使用更新版本(v0.22+),
localDescription()返回std::optional<rtc::Description>,需要用.has_value()和.value()处理
Q5:想让视频循环播放
main.cpp 中 FFmpeg 已经使用 -stream_loop -1 参数,视频会自动循环播放。
Q6:想支持多个客户端同时观看
当前代码只支持单客户端。扩展方法:
- 为每个信令请求创建独立的 PeerConnection 和推流线程
- 或使用一个推流线程向多个 PeerConnection 的 track 发送数据
- 用
std::vector<std::shared_ptr<rtc::PeerConnection>>管理多个连接
Q7:FFmpeg 子进程没有正常退出
程序退出时会自动向 FFmpeg 子进程发送 SIGTERM。如果残留了僵尸进程:
bash
pkill -f "ffmpeg.*rtp://127.0.0.1"
进阶扩展
添加音频支持
在 createOffer() 中添加音频 Track:
cpp
auto audioDesc = rtc::Description::Audio("audio",
rtc::Description::Direction::SendOnly);
audioDesc.addOpusCodec(111);
auto audioTrack = g_pc->addTrack(audioDesc);
FFmpeg 命令添加音频流:
bash
ffmpeg -re -stream_loop -1 -i video.mp4 \
-c:v copy -c:a libopus -ar 48000 -ac 2 \
-f rtp rtp://127.0.0.1:VIDEO_PORT \
-f rtp rtp://127.0.0.1:AUDIO_PORT
然后在推流线程中创建两个 UDP socket 分别接收视频和音频 RTP 包。
添加 STUN 服务器(跨网段)
在 rtc::Configuration 中添加:
cpp
config.iceServers.emplace_back("stun:stun.l.google.com:19302");
Docker 部署
创建 Dockerfile:
dockerfile
FROM ubuntu:24.04
RUN apt update && apt install -y build-essential cmake ffmpeg ...
# ... 编译和安装步骤
EXPOSE 8080
CMD ["./webrtc_streamer", "/data/video.mp4", "8080"]
总结
走过的弯路
最初的方案(也是网上许多教程的做法)是本指南的旧版本:
- 用
H264RtpPacketizer手动将 H.264 NAL 单元打包成 RTP - 调用
track->setMediaHandler()设置媒体处理链 - 浏览器创建 Offer,服务端创建 Answer
这个方案在 libdatachannel v0.21.2 上完全不能工作 ,表现为浏览器黑屏或有 ontrack 事件但无画面。根因是 libdatachannel 内部的 setMediaHandler 机制与手动调 outgoingChain 存在冲突,导致 RTP 包被二次处理而损坏。
正确的方案
参照 libdatachannel 官方 examples/media-sender 的写法:
| 职责 | 由谁完成 | 方式 |
|---|---|---|
| H.264 编码 | FFmpeg | -c:v libx264 或 -c:v copy(源已是 baseline) |
| RTP 打包 | FFmpeg | -f rtp rtp://127.0.0.1:PORT |
| RTP 传输 | UDP socket | recvfrom() 接收 |
| SSRC 修复 | C++ | memcpy(buffer+8, &ourSSRC, 4) |
| WebRTC 发送 | C++ | track->send(buffer, len) 直接转发 |
| 信令 | C++ 创建 Offer | 避免 m-line 不匹配 |
一句话:不要让 libdatachannel 碰 RTP 打包,FFmpeg 已经做好了。C++ 只做三件事------收 UDP 包、改 SSRC、转发。
关键调试技巧
cat /tmp/ffmpeg.log--- 第一个要看的,能回答"FFmpeg 到底在不在推流"pkill -f ffmpeg; pkill -f webrtc--- 出问题时先清理僵尸进程- 浏览器 F12 Console --- 看 ontrack 是否触发、ICE 状态变化
- 检查视频编码 ---
ffprobe确认是 H.264 baseline,否则-c:v copy会静默失败 - 如果换视频文件后黑屏 --- 十有八九是编码不兼容,重新用第4步转码