
简介本资源是面向嵌入式音视频开发工程师与海思平台开发者的技术实践包聚焦于在Hi3516芯片上完成live555库的完整移植与RTSP流媒体服务构建解决海思平台下实时视频流转发的核心难题。压缩包共47个文件涵盖11个.o目标文件、8个.h头文件、7个.c源文件、6个.hh C头文件及Makefile、gdb调试脚本、rtsp主程序与海思专用适配模块如rtsp_hisi.c/.h、LiveDeviceSource.cpp等全面支撑交叉编译、共享内存数据接入、RTP封装及RTSP信令交互等关键环节。已有1817人学习下载资源结构清晰含编译脚本makecompile、调试工具链gdb-arm-hisiv300-linux、gdbserver及多版本Makefilehead/tail便于快速复现移植流程、定位平台适配问题并开展二次开发。1. 项目缘起当RTSP流媒体服务器遇上海思3516最近在做一个安防监控相关的项目核心需求是在海思Hi3516DV300这颗芯片上实现一个稳定、高效的RTSP流媒体服务器。客户的要求很明确设备采集到的H.264/H.265视频流需要能够被市面上主流的VLC、FFplay、EasyPlayer等播放器通过标准的RTSP协议拉取并播放。这个需求听起来很基础但真做起来选型就成了第一个坎。市面上现成的方案不少比如用GStreamer搭一个gst-rtsp-server或者用FFmpeg的libavformat配合一些网络库自己封装。但考虑到海思平台本身的SDK已经提供了完善的媒体处理框架MPP我们更希望有一个轻量级、专注于RTSP/RTP/RTCP协议栈的解决方案能够无缝地“喂”入MPP解码后的数据。这时Live555这个老牌的开源流媒体库就进入了视野。它用C编写代码结构清晰对RTSP/RTP/RTCP协议的支持非常标准而且本身就是许多流媒体方案的基石。决定就是它了——将Live555移植到海思3516的嵌入式Linux环境中。然而“移植”二字对于嵌入式开发来说从来都不是简单的“拷贝-编译”。它意味着要让一个为x86/arm通用Linux环境设计的软件在资源受限、交叉编译工具链、内核配置乃至C库都可能存在差异的特定平台上跑起来。这个过程充满了对编译环境的驯服、对平台特性的适配以及对性能瓶颈的打磨。接下来我就把这次将Live555成功移植到海思3516平台的全过程包括踩过的坑和最终验证的稳定方案毫无保留地分享出来。2. 前期准备理解Live555与海思SDK的生态位在动手敲命令之前我们必须先理清两个核心组件的关系Live555和海思媒体处理平台MPP。这不是简单的“一个调用另一个”而是需要明确它们各自的分工和交互边界。Live555的角色纯粹的流媒体协议栈Live555的核心价值在于它实现了完整的RTSP、RTP、RTCP协议族。你可以把它理解为一个非常专业的“网络通信管家”。它的工作流程是监听在指定端口默认554监听RTSP连接请求。协商与客户端进行RTSPDESCRIBE,SETUP,PLAY等命令交互协商传输协议RTP over UDP/TCP、音视频格式、端口等。封装与发送将给定的音视频数据按照RTP包的格式进行封装并通过网络发送出去。控制通过RTCP包接收客户端反馈进行简单的流量控制和质量报告。关键在于Live555本身不负责生产音视频数据。它需要你提供一个“数据源”。这个数据源可以是一个文件如.264、.mp3也可以是一段内存中的缓冲区。Live555的框架会定时从这个数据源“拉取”数据然后走上述的协议流程发送出去。海思MPP的角色强大的媒体数据生产者海思SDK中的MPP则是顶级的“媒体内容生产车间”。它负责视频输入VI从摄像头Sensor采集原始图像数据。视频处理VPSS对图像进行缩放、降噪、增强等处理。视频编码VENC将处理后的图像压缩成H.264/H.265码流。获取编码数据通过回调函数或主动查询从VENC模块获取到一帧一帧的压缩后的码流数据通常是一个NALU单元。我们的任务搭建数据管道因此移植的核心任务就是在海思MPP数据生产者和Live555数据发送者之间搭建一条高效、低延迟的数据管道。架构图在脑海里应该是这样的海思摄像头 - VI - VPSS - VENC - [我们编写的适配层] - Live555 FramedSource - RTP打包 - 网络这个[我们编写的适配层]就是本次移植需要完成的主要编码工作。它需要继承Live555中的FramedSource类在其doGetNextFrame()函数中填入从海思VENC模块获取到的最新一帧数据。环境确认清单在开始编码前请确保你的海思3516开发环境已就绪海思SDK获取对应Hi3516DV300的完整SDK包通常包含交叉编译工具链、内核头文件、MPP库文件及样例代码。交叉编译工具链确认工具链路径例如arm-himix200-linux-gcc。使用arm-himix200-linux-gcc -v检查版本。Live555源码从官方仓库live555.com下载最新稳定版源码包如live.2024.01.10.tar.gz。网络环境开发板与主机网络互通能ping通且开发板防火墙已开放554RTSP、以及用于RTP/UDP的动态端口范围通常为60000-65000。注意海思SDK通常提供两个版本的MPP一个用于用户态较新一个用于内核态。我们这里使用用户态的MPP库进行开发更为灵活。3. 交叉编译Live555解决依赖与配置陷阱拿到Live555源码后第一步是让它能在我们的海思工具链下编译通过。Live555提供了非常灵活的genMakefiles脚本来配置编译环境但针对嵌入式平台我们需要自己编写一个编译配置头文件。步骤一解压与创建平台配置文件tar -zxvf live.2024.01.10.tar.gz cd live在live目录下创建一个针对海思平台的配置文件例如config.hisi3516cat config.hisi3516 EOF CROSS_COMPILE?arm-himix200-linux- COMPILE_OPTS $(INCLUDES) -I. -I/opt/hisi-linux/x86-arm/arm-himix200-linux/target/usr/include -O2 -DSOCKLEN_Tsocklen_t -DNO_SSTREAM1 -D_LARGEFILE_SOURCE1 -D_FILE_OFFSET_BITS64 -fPIC C c C_COMPILER $(CROSS_COMPILE)gcc C_FLAGS $(COMPILE_OPTS) CPLUSPLUS cpp CPLUSPLUS_COMPILER $(CROSS_COMPILE)g CPLUSPLUS_FLAGS $(COMPILE_OPTS) -Wall -DBSD1 OBJ o LINK $(CROSS_COMPILE)g -o LINK_OPTS -L/opt/hisi-linux/x86-arm/arm-himix200-linux/target/usr/lib -lpthread CONSOLE_LINK_OPTS $(LINK_OPTS) LIBRARY_LINK $(CROSS_COMPILE)ar cr LIBRARY_LINK_OPTS LIB_SUFFIX a LIBS_FOR_CONSOLE_APPLICATION LIBS_FOR_GUI_APPLICATION EXE EOF这个配置文件做了几件关键事CROSS_COMPILE指定交叉编译工具链前缀。-I和-L选项指向海思工具链的sysroot目录下的头文件和库路径。这是最容易出错的地方路径必须根据你实际的SDK安装位置修改。-fPIC生成位置无关代码方便后续可能需要的动态库链接。-DNO_SSTREAM1在某些嵌入式C库中std::stringstream可能有问题此定义可避免使用它。步骤二生成Makefile并编译./genMakefiles hisi3516 make -j4如果一切顺利你会在当前目录下看到编译出的静态库libBasicUsageEnvironment.a,libgroupsock.a,libliveMedia.a,libUsageEnvironment.a以及一些测试程序如testRTSPClient,testH264VideoStreamer。常见编译问题与解决std::string相关错误海思工具链可能使用较老或裁剪过的C库。如果遇到std::string找不到尝试在config.hisi3516的CPLUSPLUS_FLAGS中添加-DNO_STRING1这会让Live555使用自己的strDup等函数替代C string。inet_aton未定义某些嵌入式Linux的C库可能移除了这个函数。解决方法是修改Live555源码groupsock/inet.c找到inet_aton的实现或将其替换为inet_pton需注意IPv4/IPv6兼容性。一个简单的补丁是#ifndef HAVE_INET_ATON int inet_aton(const char *cp, struct in_addr *inp) { return inet_pton(AF_INET, cp, inp) 1; } #endif链接时找不到pthread库确保LINK_OPTS中包含了-lpthread并且工具链的库路径正确。编译成功后将四个静态库和必要的头文件BasicUsageEnvironment/include,groupsock/include,liveMedia/include,UsageEnvironment/include拷贝到你的项目目录中作为后续链接依赖。4. 核心适配实现海思MPP数据源类这是整个移植工程的灵魂所在。我们需要创建一个新的C类它同时继承自Live555的FramedSource并能够与海思MPP的VENC模块对接。4.1 类设计Hi3516H264VideoSource我们创建一个名为Hi3516H264VideoSource的类。它的核心职责是当Live555框架需要数据来填充下一个RTP包时即doGetNextFrame()被调用它能立即从海思VENC的输出缓冲区中获取一帧H.264码流数据。头文件 (Hi3516H264VideoSource.hh) 概要#ifndef _HI3516_H264_VIDEO_SOURCE_HH #define _HI3516_H264_VIDEO_SOURCE_HH #include liveMedia/FramedSource.hh #include hi_comm_venc.h #include mpi_venc.h class Hi3516H264VideoSource: public FramedSource { public: static Hi3516H264VideoSource* createNew(UsageEnvironment env, VENC_CHN VeChn); // 设置获取编码数据流的回调或信号 void setStreamDataCallback(void (*callback)(void* clientData, unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime, unsigned durationInMicroseconds), void* clientData); protected: Hi3516H264VideoSource(UsageEnvironment env, VENC_CHN VeChn); virtual ~Hi3516H264VideoSource(); private: // 最重要的函数当Live555需要数据时会调用此函数 virtual void doGetNextFrame(); // 海思VENC通道号 VENC_CHN mVeChn; // 用于从海思获取数据的相关缓冲区信息 VENC_STREAM_S mStream; VENC_PACK_S mPack; // 数据到达回调由外部MPP线程触发 void (*fDataCallback)(void* clientData, unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime, unsigned durationInMicroseconds); void* fCallbackClientData; }; #endif4.2 关键实现doGetNextFrame() 与数据获取策略doGetNextFrame()的实现是性能的关键。Live555是单线程事件驱动的这个函数会在事件循环中被调用。我们不能在这里阻塞等待一帧新的编码数据否则会卡死整个事件循环。因此我们需要一种“生产者-消费者”的异步模型。方案一回调驱动推荐这是更高效的方式。我们在海思MPP初始化时为VENC通道注册一个回调函数。当一帧数据编码完成海思的底层驱动会在一个独立的线程或中断上下文中调用这个回调。在这个回调里我们不直接处理数据而是通过信号量、环形缓冲区或简单的标志位通知Live555的主事件循环“数据准备好了”。在Hi3516H264VideoSource中void Hi3516H264VideoSource::doGetNextFrame() { if (!fDataCallback) { // 没有新数据延迟一段时间再尝试避免空转消耗CPU nextTask() envir().taskScheduler().scheduleDelayedTask(10000, (TaskFunc*)getNextFrame, this); return; } // 假设通过回调已经将数据指针和大小存放在了成员变量中 unsigned frameSize mPack.len; if (frameSize fMaxSize) { // fMaxSize是Live555传入的缓冲区大小 fFrameSize fMaxSize; fNumTruncatedBytes frameSize - fMaxSize; // 记录被截断的字节数 } else { fFrameSize frameSize; fNumTruncatedBytes 0; } // 拷贝数据到Live555的缓冲区 fTo memcpy(fTo, mPack.addr, fFrameSize); // 设置呈现时间戳。这里是个难点海思MPP通常提供的是帧的PTS90kHz时钟。 // 我们需要将其转换为微秒并填充到fPresentationTime。 // 例如mPack.pts 是90kHz的时钟计数 fPresentationTime.tv_sec mPack.pts / 90000; fPresentationTime.tv_usec (mPack.pts % 90000) * (1000000 / 90000); // 通知Live555框架数据已就绪可以进行后续处理RTP打包 FramedSource::afterGetting(this); }而数据到达回调由海思MPP线程触发// 这是一个C风格的函数供海思MPP回调 void VencDataCallback(VENC_CHN VeChn, VENC_DATA_TYPE_S *pstDataType, VENC_STREAM_S *pstStream, void *pPrivateData) { Hi3516H264VideoSource* source (Hi3516H264VideoSource*)pPrivateData; // 只处理视频帧 if (pstDataType-eDataType ! VENC_DATA_TYPE_FRAME) return; // 保存流数据到source的成员变量中注意线程安全 // 这里需要加锁或使用无锁队列因为生产者和消费者在不同线程 pthread_mutex_lock(source-mMutex); // 复制pstStream到source-mStream (简化处理通常只取第一包) if (pstStream-pstPack-len 0) { source-mPack *(pstStream-pstPack); // 浅拷贝注意地址有效性实际项目需深拷贝或引用计数。 } source-mDataReady 1; pthread_mutex_unlock(source-mMutex); // 通知Live555事件循环有数据可读 // 由于Live555环境(UsageEnvironment)可能不是线程安全的我们通过其任务调度器安排一个任务在主循环中执行 source-envir().taskScheduler().triggerEvent(HI3516_DATA_READY_EVENT, source); }在Live555的环境类中我们需要处理这个自定义事件HI3516_DATA_READY_EVENT并调用Hi3516H264VideoSource的getNextFrame()相关逻辑。方案二轮询查询如果回调方式调试复杂可以采用一种更简单但效率稍低的轮询方式。在doGetNextFrame()中直接调用海思的HI_MPI_VENC_GetStream函数非阻塞模式查询是否有新的码流。如果有则取出数据如果没有则延迟一小段时间如5ms后再次尝试。这种方式实现简单但会增加延迟和CPU占用。重要提示无论哪种方案内存拷贝和线程同步都是性能瓶颈和bug高发区。务必确保从海思MPP获取的数据缓冲区mPack.addr在拷贝完成前保持有效。海思SDK的示例代码通常使用HI_MPI_VENC_ReleaseStream来释放缓冲区你需要仔细规划释放时机避免Live555还在使用数据时缓冲区就被释放。5. 整合与RTSP服务器搭建有了数据源接下来就是搭建一个完整的RTSP服务器。Live555提供了一个很好的基础类RTSPServer和ServerMediaSession。5.1 创建服务器媒体会话在主程序中我们需要创建一个ServerMediaSession并将我们的Hi3516H264VideoSource封装成H264VideoStreamServerMediaSubsession。#include liveMedia.hh #include Hi3516H264VideoSource.hh int main(int argc, char** argv) { // 1. 初始化海思MPP系统及VENC通道 // ... (HI_MPI_SYS_Init, HI_MPI_VENC_CreateChn等) // 2. 初始化Live555任务调度器和环境 TaskScheduler* scheduler BasicTaskScheduler::createNew(); UsageEnvironment* env BasicUsageEnvironment::createNew(*scheduler); // 3. 创建我们的数据源 VENC_CHN vencChn 0; // 假设使用通道0 Hi3516H264VideoSource* videoSource Hi3516H264VideoSource::createNew(*env, vencChn); // 4. 创建H264视频流媒体子会话 // Live555需要知道SPS和PPS信息来生成SDP描述。我们需要从海思VENC获取这些参数。 // 通常可以在创建VENC通道后通过 HI_MPI_VENC_GetH264Param 获取。 VENC_H264_PARAM_S h264Param; HI_MPI_VENC_GetH264Param(vencChn, h264Param); // 将h264Param中的SPS、PPS数据提取出来转换为字符串格式... u_int8_t const sps[] { ... }; // 从h264Param获取 unsigned spsSize ...; u_int8_t const pps[] { ... }; // 从h264Param获取 unsigned ppsSize ...; H264VideoStreamDiscreteFramer* videoFramer H264VideoStreamDiscreteFramer::createNew(*env, videoSource); H264VideoStreamServerMediaSubsession* smss H264VideoStreamServerMediaSubsession::createNew(*env, videoFramer, sps, spsSize, pps, ppsSize); // 5. 创建服务器媒体会话并添加子会话 ServerMediaSession* sms ServerMediaSession::createNew(*env, Hi3516LiveStream, Live Stream from Hi3516, Session from Hi3516 Camera); sms-addSubsession(smss); // 6. 创建RTSP服务器 RTSPServer* rtspServer RTSPServer::createNew(*env, 554); // 监听554端口 if (rtspServer NULL) { *env Failed to create RTSP server: env-getResultMsg() \n; exit(1); } rtspServer-addServerMediaSession(sms); // 7. 打印访问URL char* url rtspServer-rtspURL(sms); *env RTSP server is ready at URL: url \n; delete[] url; // 8. 进入事件循环 env-taskScheduler().doEventLoop(); // 永不返回 return 0; }5.2 SPS/PPS的获取与处理这是连接海思编码器和Live555 SDP描述的关键。H.264码流的解码需要序列参数集SPS和图像参数集PPS。海思VENC在编码第一帧I帧之前通常就已经生成了SPS和PPS。获取方式有两种通过API获取如上面代码所示使用HI_MPI_VENC_GetH264Param。但需要注意这个结构体里的SPS/PPS可能不是标准的NALU格式可能需要你手动添加起始码0x00000001或根据长度字段进行拼接。从码流中提取在VENC的数据回调中第一包数据往往就是SPS第二包是PPS。你可以将它们保存下来。更可靠的做法是在回调中检查pstPack-DataType海思通常定义了VENC_PACK_TYPE_SPS、VENC_PACK_TYPE_PPS等枚举值。6. 性能调优与稳定性实战要点将程序跑通只是第一步要让它在资源受限的3516上稳定、低延迟地运行还需要一系列调优。6.1 内存与缓冲区管理避免频繁内存分配在doGetNextFrame()或数据回调中绝对不要使用new/malloc。应该在初始化时就分配好足够大的循环缓冲区。海思MPP的VENC_PACK_S中的addr指针指向的是编码器内部的一帧数据缓冲区通常是DMA内存这个缓冲区的生命周期由HI_MPI_VENC_ReleaseStream控制。我们的适配层在将数据拷贝到Live555的fTo缓冲区后需要尽快释放海思的缓冲区否则会迅速耗尽编码器的输出缓冲池导致编码线程阻塞。深拷贝与零拷贝的权衡为了安全我们采用了memcpy进行深拷贝这带来了内存带宽消耗。如果对延迟要求极致可以尝试零拷贝将海思的DMA缓冲区地址直接赋给fTo并确保在Live555发送完该帧数据后再释放海思缓冲区。这需要精细控制缓冲区生命周期难度较大。6.2 时间戳同步网络视频流的平滑播放依赖于准确的时间戳。海思MPP提供的PTS是基于90kHz时钟的。在doGetNextFrame()中我们需要将其转换为微秒1,000,000 us。公式为presentationTimeInMicroseconds pts * 1000000 / 90000如果海思的PTS不是从0开始或者有跳变你需要一个稳定的时钟基准来进行换算避免客户端播放器出现音画不同步或跳帧。6.3 网络传输优化RTP over UDP vs TCPLive555默认使用RTP over UDP。在局域网内UDP效率更高延迟更低。但在复杂的网络环境下如Wi-Fi有丢包可能会造成花屏。可以启用RTP over TCP在RTSP的SETUP命令中协商它会将RTP包封装在RTSP TCP连接中传输抗丢包能力强但延迟稍高。Live555通过ServerMediaSubsession的createNewStreamSource和createNewRTPSink等虚函数支持这种选择。MTU与分片一个H.264的NALU可能超过网络MTU通常1500字节。Live555的H264VideoStreamDiscreteFramer和H264VideoRTPSink会自动处理RTP分片。你需要确保socket的发送缓冲区足够大避免因发送阻塞导致延迟累积。6.4 多客户端并发Live555的RTSPServer本身支持多客户端并发连接。但对于我们自定义的数据源Hi3516H264VideoSource默认情况下每个客户端连接都会创建一个新的数据源实例这意味着每个客户端都会独立地去海思MPP拉取数据流这显然是不合理的会严重消耗编码器资源。解决方案是实现单数据源多路分发。我们需要修改设计让Hi3516H264VideoSource作为一个单例存在。当有新的RTSP客户端连接并创建ServerMediaSubsession时传入的是这个共享数据源的引用而不是新建一个。同时数据源内部需要维护一个客户端列表在doGetNextFrame被任一客户端触发获取到数据后需要将这份数据“广播”到所有活跃的客户端会话中。这涉及到Live555框架中FramedSource和Groupsock的更深入使用例如使用MultiFramedRTPSink的机制或自定义一个分发器。7. 踩坑实录从编译到推流的典型问题7.1 海思MPP与Live555的线程冲突这是最隐蔽的问题之一。海思MPP的数据回调函数运行在MPP自己的线程可能是内核线程或驱动线程中而Live555的事件循环运行在主线程。如果在回调中直接调用Live555对象的方法如envir().taskScheduler()极有可能造成死锁或数据竞争。我的经验是在MPP回调中只做最少的事——将数据指针和大小存入一个线程安全的队列并记录一个“有新数据”的标志。然后通过一个管道pipe、信号量semaphore或者更简单的通过Live555环境自带的triggerEvent机制向主事件循环发送一个“自定义事件”。在主事件循环处理这个自定义事件的回调函数中再去安全地操作Live555的对象和触发数据读取。7.2 SDP描述信息错误导致播放器无法解码现象VLC能连接RTSP服务器能收到PLAY响应但黑屏无法解码。 排查用Wireshark抓包查看RTSPDESCRIBE响应中的SDP信息。重点看m行媒体类型和afmtp行格式参数。检查SPS和PPS是否正确嵌入SDP。Live555的H264VideoStreamServerMediaSubsession会在SDP的afmtp属性中携带sprop-parameter-sets其值是SPS和PPS的Base64编码。确保你传入的SPS/PPS数据是完整的包含起始码。验证Profile和Level。海思编码器默认的Profile可能是High 4.1某些老旧播放器可能不支持。可以在HI_MPI_VENC_SetH264Param中尝试设置为更通用的Baseline 4.1。7.3 延迟过高超过500ms原因可能有多方面编码器GOP过大海思VENC的GOP关键帧间隔设置过长如250帧。虽然节省带宽但客户端首屏时间和seek时间会变长。对于实时监控建议GOP设置为帧率的1-2倍如25-50帧。缓冲区堆积数据从VENC回调到Live555发送中间如果有队列且消费速度慢于生产速度会导致延迟线性增长。务必确保你的数据管道是畅通的如果发现队列长度持续增长需要检查网络发送是否阻塞或者doGetNextFrame是否被及时调用。doGetNextFrame延迟调度如果采用轮询方式且查询间隔设置过长如50ms会直接引入固定延迟。尽量缩短轮询间隔或改用回调事件通知机制。7.4 运行一段时间后崩溃或内存泄漏检查资源释放确保每一个HI_MPI_VENC_GetStream都有对应的HI_MPI_VENC_ReleaseStream。可以在代码中成对打印日志。Live555对象生命周期Live555的对象依赖关系复杂确保FramedSource,MediaSession,RTSPServer等在程序退出时被正确销毁。遵循“谁创建谁删除”的原则对于通过createNew()静态方法创建的对象Live555通常有对应的close()或delete方法。使用内存检测工具将编译好的程序放在海思板子上使用valgrind如果板子支持或通过观察/proc/meminfo中Slab和SUnreclaim项的变化来排查内存泄漏。整个移植过程是对嵌入式开发、网络协议和多媒体处理的一次综合考验。从最初编译通不过的烦躁到后来画面成功推流出来的兴奋再到最后为优化那100毫秒延迟而反复打磨的执着每一步都充满了挑战和收获。最终当用手机上的VLC流畅地看到来自海思3516摄像头的实时画面时所有的努力都值了。希望这份详细的记录能帮你绕过我踩过的那些坑更顺畅地完成自己的移植项目。本文还有配套的精品资源点击获取