嵌入式Linux智能电视应用开发:实现本地视频的硬解码与网络投屏功能
智能电视应用不是“开个播放器+装个投屏App”拼起来就行。本地播放走硬件解码器,投屏又要收网络码流再解码,两条路若各开各的显示通道,就会抢DRM图层、重复拷贝、音画不同步。正确架构是:解封装/收流 → 硬解码 → DMA-BUF帧 → 统一送DRM合成上屏。
一、本地视频硬解码管线
嵌入式ARM平台别指望VA-API,主流是V4L2 M2M或厂商VPU(RK用MPP、i.MX用VPU、全志用VE)。流程:
demux分离H.264/H.265/AAC
视频包送硬解
解码器输出YUV/NV12帧
导出dma-buf fd
DRM Plane直接扫描输出
GStreamer快速原型:
gst-launch-1.0 filesrc location=1.mp4 ! qtdemux ! \
v4l2slh264dec ! videoconvert ! kmssink
产品化建议自己写解封装,只调v4l2_m2m或MPP,buffer流转可控。
核心零拷贝片段:
/* V4L2 M2M取解码帧 */
struct v4l2_buffer buf = {.type=V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,
.memory=V4L2_MEMORY_MMAP};
ioctl(dec_fd, VIDIOC_DQBUF, &buf);
/* 导出DMA-BUF */
struct v4l2_exportbuffer exp = {.type=buf.type,.index=buf.index};
ioctl(dec_fd, VIDIOC_EXPBUF, &exp); // exp.fd
/* DRM导入 */
uint32_t handle;
drmPrimeFDToHandle(drm_fd, exp.fd, &handle);
drmModeAddFB2(drm_fd, w, h, DRM_FORMAT_NV12,
&handle, &pitch, &off, &fb_id, 0);
drmModeSetPlane(drm_fd, plane_id, crtc_id, fb_id,
0,0,0,0,0,0,w<<16,h<<16);
CPU不参与像素搬运,4K也压得住。
二、投屏两条路怎么选
DLNA(UPnP AV):适合点播本地/网盘媒体
SSDP发现设备
SOAP控制播放/暂停
HTTP拉流或拉取原文件
电视端本质是“再起一个播放器”
轻量方案用gmrender-resurrect+GStreamer,或MiniDLNA做服务端、自己写renderer。
Miracast/WFD:手机镜像屏幕
Wi-Fi P2P组网
RTSP协商SDP(M1~M8)
RTP over UDP收H.264
Sink硬解渲染
Linux下Miracast常卡在Wi-Fi Direct驱动,量产电视一般用厂商WFD Sink方案。
三、投屏接收线程
Miracast收流关键不是“收到就解”,是RTP组帧+丢包保护:
void rtp_recv_loop(int sock){
uint8_t pkt[1500];
while(recvfrom(sock,pkt,sizeof(pkt),0,&addr,&len)>0){
uint16_t seq = ntohs(*(uint16_t*)(pkt+2));
if(seq != expected){ // 丢包
mark_frame_incomplete(); // 等下一个IDR再送解
expected = seq+1; continue;
}
append_nal(pkt+12, len-12); // 去RTP头
expected++;
}
}
H.264单NAL模式直接拼NAL;SPS/PPS变化要更新解码器csd,不然黑屏。
解码出来同样是dma-buf帧,和本地播放走同一套DRM合成。
四、协同调度核心
别让本地播放和投屏各开线程直接写屏,冲突必花屏。统一模型:
typedef struct {
int dma_fd;
uint32_t w,h;
int64_t pts;
src_type_t src; // LOCAL / CAST
} frame_t;
QueueHandle_t disp_q;
/* 本地解码线程 */
disp_q_push(decode_local_frame());
/* 投屏解码线程 */
disp_q_push(decode_rtp_frame());
/* 单一显示线程 */
void disp_task(void*){
frame_t f;
while(xQueueReceive(disp_q,&f,portMAX_DELAY)){
drm_show_frame(f.dma_fd,f.w,f.h);
av_sync_check(f.pts); // 本地才用音频时钟
release_dec_buf(f);
}
}
规则:
显示只由一个线程管
本地播放带音频时钟做AV同步
投屏镜像以视频RTP时间戳为准,音频另路混音
切换信源先drmModeSetPlane禁用旧层,再上新房
投屏结束回收V4L2 buffer,别留dma-buf泄漏
五、状态机
typedef enum { IDLE, LOCAL_PLAY, CASTING, SWITCHING } app_st;
本地播放中收到投屏请求 → 暂停本地解码线程,保留进度
投屏结束 → 恢复本地或回待机
来电/暂停 → 本地进PAUSE,投屏发RTSP PAUSE
Wi-Fi断连 → RTP超时100ms判断流,清屏回UI
六、落坑清单
解码帧memcpy到fb0→CPU爆、断帧
本地和投屏同时drmModeSetCrtc→图层互踩
RTP丢包直接喂解码器→花屏卡死
SPS变更不更新→后续全黑
Miracast依赖Wi-Fi Direct驱动缺失→连不上
投屏也走音频时钟同步→镜像延迟飘
dma-buf用完不关fd→几小时泄漏
GStreamer默认vo=x11→嵌入式无桌面直接废
切换信源不释放plane→残影
七、落地顺序
先跑通本地V4L2 M2M硬解+DRA显示
封装dma-buf帧队列
显示线程统一调度
DLNA renderer接同一队列(最快出Demo)
Miracast走P2P+RTSP+RTP收流
RTP组帧+丢包等到IDR
信源切换状态机
音量/按键/CEC事件分发
智能电视“本地+投屏”的本质不是两个功能并存,是多条码流进同一个硬解显示管线。本地文件解封装、DLNA拉流、Miracast收RTP,到最后都是H.264/H.265包喂解码器,出来dma-buf帧交给DRM Plane。协同的关键在“显示归一、解码并流、切换有状态”——谁直接写屏谁就背锅,单一显示线程+帧队列是避坑底线。





