MediaMTX 移动端拉流实战:WebRTC 与 LL-HLS 两条路径
MediaMTX 移动端拉流实战WebRTC 与 LL-HLS 两条路径【免费下载链接】mediamtxReady-to-use Media-over-QUIC / SRT / WebRTC / RTSP / RTMP / LL-HLS / MPEG-TS / RTP live media server and media proxy that allows to read, publish, proxy, record and playback real-time video and audio streams.项目地址: https://gitcode.com/GitHub_Trending/me/mediamtxiOS 或 Android App 要从 MediaMTX 拉一路低延迟流客户端只需实现两个 HTTP 端点WHEPWebRTC 信令和 HLS。WebRTC 端到端延迟约 0.5sLL-HLS 约 2s服务端零额外编译默认配置即可开跑。30 秒速览本文涉及的全部地址与端口# WebRTC 信令WHEPHTTP 握手媒体走 UDP POST http://服务器IP:8889/mystream/whep # HLS / LL-HLS 播放列表 http://服务器IP:8888/mystream/index.m3u8 # 用 FFmpeg 推一路测试流 ffmpeg -re -stream_loop -1 -i file.mp4 -c copy -f rtsp rtsp://localhost:8554/mystream选型决策表方案延迟兼容性加密适用场景WebRTC / WHEP端口 8889 UDP 8189~0.5siOS 14 / Android需 WebRTC SDK媒体流始终经 DTLS-SRTP 加密实时互动、低延迟监控HLS / LL-HLS端口 8888~2shlsPartDuration: 200ms 1s 分段iOS 系统 AVPlayer / Android ExoPlayer可选 HTTPS纯观看、弱网、走 CDN结论要求实时交互选 WebRTC/WHEP只读画面、要穿 CDN 或弱网选 LL-HLS。RTSP、RTMP 等协议在移动端的 SDK 生态都不如这两条路径本文不再展开。打开 WHEP 与 LL-HLS 服务端口mediamtx.yml 中webrtc和hls默认就是true下面是最小改动集webrtc: enabled: true webrtcLocalUDPAddress: :8189 # 媒体流 UDP 端口默认值 hls: enabled: true hlsVariant: lowLatency # mpegts / fmp4 / lowLatency 三选一 hlsSegmentDuration: 1s hlsPartDuration: 200ms hlsEncryption: false # LL-HLS 在 Apple 设备上要求 HTTPS需要放行的端口WebRTC 信令 8889TCP、媒体 8189UDP、HLS 8888TCP。写客户端代码前先用一条命令确认服务端就绪curl -s -o /dev/null -w %{http_code}\n http://服务器IP:8888/mystream/index.m3u8有流返回 200无流返回 404——404 也说明服务已起来再推一路测试流ffmpeg -re -stream_loop -1 -i file.mp4 -c copy -f rtsp rtsp://服务器IP:8554/mystream浏览器直接访问http://服务器IP:8889/mystream会打开 MediaMTX 内置的 WebRTC 播放页可先确认链路再写原生代码。iOS 集成WebRTCPOST SDP 到 WHEP 端点Podfile 加一行依赖pod GoogleWebRTC信令共两步OPTIONS /mystream/whep拉 ICE 服务器配置POST /mystream/whep提交 offer服务端返回 201body 是 answer SDPLocation响应头是后续 trickle ICE 用的会话 URL。核心类如下import WebRTC final class WHEPClient: NSObject, RTCPeerConnectionDelegate { let preview: RTCMTLVideoView private var pc: RTCPeerConnection? private var sessionURL: String? private var queued: [RTCIceCandidate] [] init(preview: RTCMTLVideoView) { self.preview preview } func connect(url: String) { // 拉 ICE 服务器配置服务端未配置时为空列表 var req URLRequest(url: URL(string: url)!) req.httpMethod OPTIONS URLSession.shared.dataTask(with: req) { data, resp, _ in let ice (resp as? HTTPURLResponse)?.allHeaderFields .filter { $0.key.lowercased() link }.compactMap { $0.value as? String } ?? [] self.start(url: url, iceLinks: ice) }.resume() } private func start(url: String, iceLinks: [String]) { let cfg RTCConfiguration() cfg.sdpSemantics .unifiedPlan cfg.iceServers iceLinks.map { Self.parseIceLink($0) } // ← 可选 pc RTCPeerConnectionFactory().peerConnection(with: cfg, delegate: self) pc?.addTransceiver(forMediaType: .video, init: RTCMediaStreamTrackTransceiverInit(direction: .recvonly)) pc?.addTransceiver(forMediaType: .audio, init: RTCMediaStreamTrackTransceiverInit(direction: .recvonly)) _ pc?.createDataChannel(label: , init: RTCDataChannelInit()) pc?.createOffer { [weak self] offer, _ in guard let self self, let offer offer else { return } self.pc?.setLocalDescription(offer) { _ in self.postOffer(url: url, sdp: offer.sdp) } } } }postOffer负责把 offer 发给服务端并接住 answeronLocalCandidate处理候选排队extension WHEPClient { func postOffer(url: String, sdp: String) { var req URLRequest(url: URL(string: url)!) req.httpMethod POST req.setValue(application/sdp, forHTTPHeaderField: Content-Type) req.httpBody sdp.data(using: .utf8) URLSession.shared.dataTask(with: req) { [weak self] data, resp, _ in guard let self self, let http resp as? HTTPURLResponse, http.statusCode 201 else { return } // Location 头 会话 URL之后 PATCH 候选、DELETE 会话都用它 if let loc http.value(forHTTPHeaderField: Location) { self.sessionURL URL(string: url)?.appendingPathComponent(loc).absoluteString } self.pc?.setRemoteDescription(RTCSessionDescription(type: .answer, sdp: String(data: data ?? Data(), encoding: .utf8) ?? )) { _ in self.flushCandidates() } }.resume() } func onLocalCandidate(_ c: RTCIceCandidate) { sessionURL nil ? queued.append(c) : send([c]) } func flushCandidates() { queued.forEach { _ in send(queued) }; queued [] } private func send(_ cs: [RTCIceCandidate]) { guard let sessionURL sessionURL else { return } let frag cs.compactMap { \($0.sdp) }.joined() var req URLRequest(url: URL(string: sessionURL)!) req.httpMethod PATCH req.setValue(application/trickle-ice-sdpfrag, forHTTPHeaderField: Content-Type) req.httpBody frag.data(using: .utf8) URLSession.shared.dataTask(with: req).resume() } func peerConnection(_ pc: RTCPeerConnection, didAdd track: RTCMediaStreamTrack, streams: [RTCMediaStream]) { if let video track as? RTCVideoTrack { preview.viewFromSource(video) } } }用 AVPlayer 播放 LL-HLS 流HLS 路径不需要任何第三方依赖系统 AVPlayer 直接播import AVKit let url URL(string: https://服务器IP:8888/mystream/index.m3u8)! // ← LL-HLS 在 Apple 设备上要求 HTTPS let player AVPlayer(url: url) let vc AVPlayerViewController() vc.player player vc.view.frame view.bounds view.addSubview(vc.view) player.play()注意hlsVariant: lowLatency时 iOS 端必须走 HTTPS因此线上要么给 8888 配证书hlsEncryption: true要么改用mpegts变体走 HTTP。Android 集成用 ExoPlayer 播放 HLS 流app/build.gradle加依赖implementation com.google.android.exoplayer:exoplayer:2.19.1播放列表代码class MtxHlsPlayer(private val ctx: Context) { private val player ExoPlayer.Builder(ctx).build() private var prepared false fun attach(view: PlayerView) { view.player player } fun play(url: String) { // ← 如 http://服务器IP:8888/mystream/index.m3u8 if (!prepared) { val dsf DefaultHttpDataSource.Factory() .setUserAgent(MyApp/1.0) .setConnectTimeoutMs(5000) .setReadTimeoutMs(5000) player.setMediaSource(HlsMediaSource.Factory(dsf).createMediaSource(MediaItem.fromUri(url))) player.prepare() prepared true } player.playWhenReady true } fun release() { player.release() } }一个实测差异ExoPlayer 不支持 LL-HLS 的 part 级播放给它lowLatency变体也拿不到低延迟。Android 上要压 HLS 延迟改服务端为hlsVariant: mpegtshlsSegmentDuration: 1s首帧后延迟约 2s。WebRTCGlide 信令流程只列与 iOS 的差异依赖implementation org.webrtc:google-webrtc:1.0.32006初始化Application 或页面初始化时一次PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(context) .setNativeLibraryLoader { lib - System.loadLibrary(it) })信令流程与 iOS 完全对称OPTIONS 拉 ICE 配置 → createOffer → POST 到 WHEP 端点 →setRemoteDescription→ 候选 PATCH 到Location头指向的会话 URL。差异点Android 的RtcConfiguration需要解析响应头里的Link: stun:...; relice-server拿到 ICE 服务器 URL 和凭据HttpURLConnection.getHeaderFields(Link)即可拿到视频挂到SurfaceViewRendereronAddStream回调里track.addSink(videoView)音频调用audioManager.setRemoteDisplay(...)hlsVariant: lowLatency时 Android 端没有 HTTPS 硬要求HTTP 可直接播放。踩坑与调优画面黑屏但无任何报错现象App 连上了、日志干净、画面纯黑。 根因路径下没人推流或 WHEP POST 拿到 404 被静默吞掉。 解法先curl确认index.m3u8返回 200再在 POST 回调里打印statusCode。HLS 首帧等待超过 3 秒现象拉流后要等 3s 以上才出画面。 根因播放器默认缓冲 3 个片段。 解法服务端hlsSegmentDuration: 1shlsPartDuration: 200ms见 1-configuration-file.md 中 hls 参数的说明或换 WHEP。WHEP POST 之后一直卡在 ICE 候选交换现象201 拿到了answer 也 set 了画面永远不来。 根因UDP 8189 被防火墙/NAT 挡住ICE 检查不通过。 解法放行 UDP 8189仍不通就配webrtcLocalTCPAddress: :8189走 TCP公网部署把服务器可达 IP 加进webrtcAdditionalHosts完整排查顺序见 25-webrtc-specific-features.md。iOS 真机连不上 HTTP 端点现象模拟器能连真机请求直接失败。 根因iOS 17 ATS 默认禁止明文 HTTP。 解法开发期在 Info.plist 加NSAppTransportSecurity例外正式环境把hlsEncryption/webrtcEncryption打开走 HTTPS。通用排查三步nc -vz确认 8888/8889 可达 → 抓包看 WHEP POST 是否 201、answer 里m行的媒体地址 → 翻服务端日志里的 WebRTC 握手记录。下一步 延伸阅读WebRTC 连接不通按 25-webrtc-specific-features.md 的顺序逐级处理UDP 端口 → TCP 回退 → STUN → TURN同文也列了 H.265、含 B 帧 H.264 的浏览器兼容性限制。LL-HLS 想走 CDN 时注意缓存带来的延迟回退见 19-scalability.md。hlsPartDuration、webrtcHandshakeTimeout等参数的完整清单在 1-configuration-file.md。WHEP 端点实现可直接读 internal/servers/webrtc/ 与 internal/protocols/whip/。【免费下载链接】mediamtxReady-to-use Media-over-QUIC / SRT / WebRTC / RTSP / RTMP / LL-HLS / MPEG-TS / RTP live media server and media proxy that allows to read, publish, proxy, record and playback real-time video and audio streams.项目地址: https://gitcode.com/GitHub_Trending/me/mediamtx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考