技术赋能行业变革:跨平台低延迟音视频解决方案

智慧之道,快如闪电:如何满足各行业的音视频直播需求

“功夫,终究是为了掌控时间。
你说一帧不来,是秒;我说一帧不来,是历史。
我们的世界,没有慢的奢侈,只有精准的速度。”

大牛直播SDK(SmartMediaKit)自2015年诞生起,便在音视频直播领域掀起一股风暴。
它不是简单的“技术”,它是一种系统性、可控性、精准性的解决方案。我们不仅要做“能用”的播放器,更要做一整套 能够满足行业最严苛需求的音视频系统


【一】为行业定制的技术核心:快,不止于快

在传统音视频直播技术中,“延迟”是每个开发者都绕不开的话题。
但真正的技术高手,从来不是随波逐流,而是能掌控每一帧画面到达的时刻

大牛直播SDK致力于为各行业提供毫秒级低延迟解决方案,我们深知,低延迟不仅仅是技术指标,更是业务流畅度与效率的生命线。

超低延迟,不止于快

行业内,只有为数不多的公司真正做到毫秒级的低延迟,而我们大牛直播SDK,以 100-200ms 的延迟处理,突破行业传统瓶颈,改变了以往的体验。
我们不再受限于带宽、硬件、协议的传统局限,而是通过创新性的解决方案,把直播的每一帧,都精准而及时地推送到用户眼前。

比快更快,智者领先。


【二】行业应用,深度赋能数字化转型

“你说我们做的,是直播;我说,我们做的,是数字化革命。”
大牛直播SDK不仅仅是一个“播放器”,它是一套技术架构,覆盖从数据采集到播放、从音视频处理到交互控制的全链路解决方案,并广泛应用于以下场景:

1. 在线教育与智慧教室

在现代教育中,课堂与课堂之间的距离正在被打破。
大牛直播SDK为在线教育平台提供了高效、稳定、低延迟的视频直播解决方案,支持无纸化推屏、实时互动、在线答疑等功能,保障每个学习者在屏幕前与老师同步互动。

  • 应用:在线教育平台、虚拟教室、智慧教室。

2. 智慧安防与远程医疗

“世界上的每一秒,都是无价的。”
智慧安防远程医疗领域,实时视频监控与快速响应至关重要。大牛直播SDK支持移动视频安防监控,并在远程专家诊断、远程医疗等场景中提供高清稳定的视频流。

  • 应用:移动安防、智慧门禁、智能家居、远程医疗。

3. 应急指挥与监控巡检

应急指挥是一个生死攸关的任务,时间即是生命。
大牛直播SDK通过低延迟、高稳定性的推流技术,为各类应急指挥系统(如公安、消防、城管等)提供实时视频支持,并能够在复杂网络环境下依然保持高效稳定的推送。

  • 应用:应急指挥、执法记录、管道巡检、智慧水利、电力巡检。

4. 工业互联网与物联网

在智能设备、物联网设备中,视频流的采集与处理尤为关键。
我们将大牛直播SDK与工业互联网场景深度融合,支持工业视频巡检、智能车载监控、智能设备控制等应用,通过实时视频流和数据反馈,为各类设备提供实时数据监控与调度功能。

  • 应用:电力巡检、铁路监控、智能车载、物联网设备。

【三】技术特点与功能优势:大牛直播SDK的硬核实力

大牛直播SDK深耕音视频技术多年,已经不再单纯是一个播放工具,而是一整套完整的直播解决方案,支持:

  • 多协议支持:支持 RTMP、RTSP、HTTP-FLV 等多种协议,适配不同直播需求;
  • 跨平台能力:完美支持 Windows、Android、iOS、Linux、Unity3D 等多平台,开发者无需担心设备与平台的适配;
  • 超低延迟推流与播放:毫秒级延迟优化,保证流畅的直播体验;
  • 视频解码能力:支持 H.264/H.265 软解与硬解,支持多路并行解码;
  • 音频编码:支持 AAC、SPEEX、PCMA、PCMU 等音频格式,保证音频质量;
  • 实时动态水印、快照:直播过程中可以实时添加水印或截图;
  • 支持多路流转发:RTSP 转 RTMP、RTMP 转 RTMP、RTSP | RTMP 转 GB28181,实现复杂的多流转发需求;
  • RTSP自动切换模式:自动切换 TCP/UDP 模式,应对不同网络环境;
  • 全链路监控:支持网络状态、缓冲状态等回调,实时监控直播状态;

这些能力,是支撑大牛直播SDK能够深入多个行业并提供高效解决方案的核心所在。


【四】场景扩展:从直播到互动,深度满足业务需求

大牛直播SDK不仅仅满足单向播放需求,更能为互动场景、实时沟通提供强大支持:

  • 一对一互动:支持实时音视频通信,适用于远程视频会议、在线咨询、远程教育等场景
  • 音视频导播与动态合成:支持在直播过程中对音视频内容进行导播、合成、混音,为虚拟活动、赛事直播等提供丰富的场景支持;
  • 录像与存储:支持推流端实时录像、存储,提供专业级的录制功能,方便后期编辑和分享。

这就是我们所做的,不仅仅是视频播放,而是“视频服务的全链条”。


【五】结语:智慧未来,由我们共同创造

“不变的,是时间;变的,是技术。”
大牛直播SDK用极致的低延迟与稳定性,构建了未来音视频服务的基石。我们不仅为今天的行业赋能,也为明日的技术突破留下了足够的空间。

在未来的数字化世界,所有行业的“实时”都将依赖无缝连接与智能控制
大牛直播SDK,不只是“播放工具”,它是行业数字化转型的桥梁盾牌心脏

我们不问未来,我们已为你准备好。
比快更快,智者领先

大牛直播SDK模块一览表

Windows端

  •  RTMP直播推流SDK RTMP推送SDK(支持同时推多路url,支持RTMP扩展H.265推送和Enhanced RTMP H.265);

  •  RTMP直播播放器SDK 业内首屈一指的RTMP超低延迟直播播放器SDK(支持RTMP H.265扩展播放和Enhanced RTMP H.265);

  •  RTSP直播播放器SDK 支持RTSP H.265播放及扩展录像、业内为数不多真正好用的RTSP播放器SDK,支持IE浏览器OCX控件接口调用;

  •  Unity3D RTMP/RTSP直播播放器SDK 业内为数不多的Windows支持Unity3D的超低延迟RTMP/RTSP直播播放器SDK,支持快照、录像、实时静音、view旋转、快速切换URL等特性;

  •  RTMP/RTSP多路流媒体转RTMP推送SDK 支持同时多路拉取rtmp/rtsp流/本地flv文件,并分别转发到服务器,业内为数不多支持RTSP/RTMP H.265拉流转发的SDK;

  •  轻量级RTSP服务SDK 为满足内网无纸化/电子教室等内网超低延迟需求,避免让用户配置单独的服务器,大牛直播SDK在推送端支持轻量级RTSP服务SDK,推送端SDK支持的功能,内置轻量级RTSP服务SDK后,功能继续支持,windows端特定机型支持RTSP H.265视频输出,支持单播组播模式;

  •  内网RTSP网关SDK 内网RTSP网关SDK,系内置轻量级RTSP服务SDK扩展,完成外部RTSP/RTMP数据拉取并注入到轻量级RTSP服务SDK工作,多个内网客户端直接访问内网轻量级RTSP服务获取公网数据,无需部署单独的服务器,支持RTSP/RTMP H.265数据接入,支持单播组播模式;

  •  导播SDK 数据源:1. rtmp/rtsp音视频流;2. 本地屏幕/摄像头/音频数据;3.本地flv文件;多路流合成一路实时导播推送;

  •  录像SDK 支持拉取rtmp/rtsp流实时录像模块/实时快照功能,支持纯音频、纯视频、音视频录制模式,支持音频(PCMU/PCMA,Speex等)转AAC后再录像,业内为数不多的支持RTSP/RTMP H.265录制到MP4文件的录像SDK;

  •  RTMP/RTSP一对一互动SDK 基于标准协议(RTMP或RTSP)的跨平台(Windows/Andriod/iOS)一对一互动SDK,支持回音消除,完美支持一对一互动场景;

  •  SEI扩展数据发送/接收SDK 支持推送端通过H.264 SEI信息扩展,实时传输文本/二进制数据信息(如实时字幕/时间戳/题目分发/公告广播等),播放端做相应解析和回显;

Linux|麒麟操作系统

  •  RTMP直播推流SDKRTMP推送SDK(支持推送屏幕、摄像头数据,支持支持RTMP扩展H.265推送和Enhanced RTMP H.265);

  •  RTMP直播播放器SDK业内首屈一指的RTMP超低延迟直播播放器SDK(支持RTMP H.265扩展播放和Enhanced RTMP H.265);

  •  RTSP直播播放器SDK支持RTSP H.265播放及扩展录像、业内为数不多真正好用的RTSP播放器SDK;

  •  [RTMP/RTSP多路流媒体转RTMP推送SDK]支持同时多路拉取rtmp/rtsp流,并分别转发到服务器,业内为数不多支持RTSP/RTMP H.265拉流转发的SDK;

  •  轻量级RTSP服务SDK 为满足内网无纸化/电子教室等内网超低延迟需求,避免让用户配置单独的服务器,大牛直播SDK在推送端支持轻量级RTSP服务SDK,推送端SDK支持的功能,内置轻量级RTSP服务SDK后,功能继续支持;
  •  [录像SDK]支持拉取rtmp/rtsp流实时录像模块/实时快照功能,支持纯音频、纯视频、音视频录制模式,支持音频(PCMU/PCMA,Speex等)转AAC后再录像,业内为数不多的支持RTSP/RTMP H.265录制到MP4文件的录像SDK。

Android端

  •  RTMP直播推流端SDK Android屏幕、摄像头RTMP推流SDK,支持支持RTMP扩展H.265推送和Enhanced RTMP H.265(H.265硬编码);

  •  GB28181接入SDK Android屏幕、摄像头编码注册接入到GB28181服务平台;

  •  RTMP直播播放器SDK 业内首屈一指的RTMP超低延迟直播播放器SDK(支持RTMP H.265扩展播放和Enhanced RTMP H.265);

  •  RTSP直播播放器SDK 支持RTSP H.265播放及扩展录像、业内为数不多真正好用的RTSP播放器SDK;

  •  HTTP-FLV直播播放器SDK 支持HTTP/HTTPS-FLV播放及扩展录像、比快更快,HTTP-FLV一样可以毫秒级延迟;
  •  Android平台外部编码数据实时预览播放SDK 外部编码后音视频数据(视频:H.264/H.265,音频:AAC/PCMA/PCMU)实时预览播放SDK;
  •  Unity3D RTMP/RTSP直播播放器SDK 业内为数不多的Android支持Unity3D的超低延迟RTMP/RTSP直播播放器SDK,支持快照、录像、实时静音、view旋转、快速切换URL等特性;

  •  录像SDK 支持拉取rtmp/rtsp流实时录像模块/实时快照功能,支持纯音频、纯视频、音视频录制模式,支持音频(PCMU/PCMA,Speex等)转AAC后再录像,业内为数不多的支持RTSP/RTMP H.265录制到MP4文件的录像SDK;

  •  RTMP/RTSP多路流媒体转RTMP推送SDK 支持实时拉取的rtmp/rtsp流转发到指定rtmp url;

  •  轻量级RTSP服务SDK 为满足内网无纸化/电子教室等内网超低延迟需求,避免让用户配置单独的服务器,大牛直播SDK在推送端支持轻量级RTSP服务SDK,推送端SDK支持的功能,内置轻量级RTSP服务SDK后,功能继续支持(支持H.265);

  •  RTMP/RTSP一对一互动SDK 基于标准协议(RTMP或RTSP)的跨平台(Windows/Andriod/iOS)一对一互动SDK,支持回音消除,完美支持一对一互动场景;

  •  SEI扩展数据发送/接收SDK 支持推送端通过H.264 SEI信息扩展,实时传输文本/二进制数据信息(如实时字幕/时间戳/题目分发/公告广播等),播放端做相应解析和回显。

iOS端

  •  RTMP直播推流端SDK iOS屏幕(基于ReplayKit)、摄像头RTMP推流SDK(支持RTMP扩展H.265推送和Enhanced RTMP H.265);

  •  RTMP直播播放器SDK 业内首屈一指的RTMP超低延迟直播播放器SDK(支持RTMP H.265扩展播放和Enhanced RTMP H.265);

  •  RTSP直播播放器SDK 支持RTSP H.265播放及扩展录像、业内为数不多真正好用的RTSP播放器SDK;

  •  Unity3D RTMP/RTSP直播播放器SDK 业内为数不多的iOS支持Unity3D的超低延迟RTMP/RTSP直播播放器SDK,支持快照、录像、实时静音、view旋转、快速切换URL等特性;

  •  录像SDK 支持拉取rtmp/rtsp流实时录像模块/实时快照功能,支持纯音频、纯视频、音视频录制模式,支持音频(PCMU/PCMA,Speex等)转AAC后再录像,业内为数不多的支持RTSP/RTMP H.265录制到MP4文件的录像SDK;

  •  RTMP/RTSP多路流媒体转RTMP推送SDK 支持实时拉取的rtmp/rtsp流转发到指定rtmp url;

  •  轻量级RTSP服务SDK 为满足内网无纸化/电子教室等内网超低延迟需求,避免让用户配置单独的服务器,大牛直播SDK在推送端支持轻量级RTSP服务SDK,推送端SDK支持的功能,内置轻量级RTSP服务SDK后,功能继续支持;

  •  内网RTSP网关SDK 内网RTSP网关SDK,系内置轻量级RTSP服务SDK扩展,完成外部RTSP/RTMP数据拉取并注入到轻量级RTSP服务SDK工作,多个内网客户端直接访问内网轻量级RTSP服务获取公网数据,无需部署单独的服务器,支持RTSP/RTMP H.265数据接入;

  •  RTMP/RTSP一对一互动SDK 基于标准协议(RTMP或RTSP)的跨平台(Windows/Andriod/iOS)一对一互动SDK,支持回音消除,完美支持一对一互动场景;

  •  SEI扩展数据发送/接收SDK 支持推送端通过H.264 SEI信息扩展,实时传输文本/二进制数据信息(如实时字幕/时间戳/题目分发/公告广播等),播放端做相应解析和回显。

相关模块SDK:

1. Windows平台集成对接DEMO:

  •  [推送模块] RTMP推送SDK、轻量级RTSP服务SDK、录像SDK(C++|C#) Demo工程(对应SmartPublisherDemo.exe);
  •  [播放模块] RTMP|RTSP播放SDK(C++|C#|WPF) Demo工程(对应SmartPlayer.exe);
  •  [转发模块] 多路RTSP|RTMP转RTMP推送模块SDK(C++|C#) Demo工程(对应SmartStreamRelayDemo.exe);
  •  [混流模块] 混流SDK(C++) Demo工程(对应SmartEchoCancellation.exe);
  •  [一对一互动模块] 基于标准RTMP服务的一对一互动demo工程(C#),可扩展RTSP一对一互动(对应SmartEchoCancellation.exe)。

2. Linux平台(含X86_64|aarch64架构)集成对接DEMO:

  •  [推送模块] RTMP推送SDK录像SDK(C++|C#) Demo工程(对应publisherdemo);
  •  [播放模块] RTMP|RTSP播放SDK Demo工程(对应playerdemo);
  •  [轻量级RTSP服务模块] 轻量级RTSP服务SDK Demo工程(对应rtspserverdemo);
  •  [转发模块] 多路RTSP|RTMP转RTMP推送模块SDK Demo工程(对应relaydemo);

3.Android平台集成对接DEMO:

  •  SmartPlayerV2: RTMP|RTSP|HTTP-FLV直播播放SDK demo,涵盖实时录像、快照、扩展SEI数据接收等功能;
  •  SmartPublisherV2: RTMP直播推送SDK、GB28181接入SDK、轻量级RTSP服务SDK、扩展SEI发送SDK、实时录像SDK多合一demo;
  •  Camera2PublisherV2: 基于Android平台Camera2摄像头数据采集,RTMP直播推送SDK、GB28181接入SDK、轻量级RTSP服务SDK、实时录像SDK多合一demo;
  •  SmartEchoCancellationV2: 基于RTMP的一对一互动demo(可扩展RTSP一对一互动);
  •  SmartServiceCameraPublisherV2: 后台摄像头RTMP直播推送SDK;
  •  SmartServicePublisherV2: 智慧教室|无纸化会议等屏幕、麦克风扬声器采集 、RTMP直播推送、轻量级RTSP服务demo;
  •  SmartRelayDemoV2: RTMP|RTSP直播播放SDK、RTMP|RTSP转RTMP推送SDK、RTMP|RTSP实时录像、轻量级RTSP服务四合一demo。

4.iOS平台集成对接DEMO:

  •  SmartiOSPlayerV2: RTMP|RTSP直播播放SDK demo,涵盖实时录像、快照、扩展SEI数据接收等功能;
  •  SmartiOSPublisherV2: RTMP直播推送SDK、RTSP直播推送SDK、轻量级RTSP服务SDK、扩展SEI发送SDK、实时录像SDK多合一demo;
  •  SmartiOSEchoCancellation: 基于RTMP的一对一互动demo(可扩展RTSP一对一互动);
  •  SmartiOSScreenPublisherV2: 基于ReplayKit采集的智慧教室|无纸化会议等屏幕采集(推屏) 、RTMP直播推送demo;
  •  SmartiOSRelayDemoV2: RTMP|RTSP直播播放SDK、RTMP|RTSP转RTMP推送SDK、RTMP|RTSP实时录像、轻量级RTSP服务四合一demo。

大牛直播SDK集成和调用说明

平台扩展

除了Windows/Linux/Android/iOS Native SDK,大牛直播SDK发布了Unity环境下的RTMP推流|轻量级RTSP服务(Windows平台+Linux平台+Android平台)和RTMP|RTSP拉流(Windows、Linux、Android和iOS平台全覆盖)低延迟的解决方案。

目前,大牛直播SDK的Unity3D环境下,已覆盖以下SDK:

  •  Windows平台RTMP直播推送SDK(采集Unity窗体、摄像头或屏幕);
  •  Windows平台轻量级RTSP服务SDK(采集Unity窗体、摄像头或屏幕);
  •  Windows平台RTMP|RTSP直播播放SDK;
  •  Linux平台RTMP直播推送SDK(采集Unity窗体、Unity声音);
  •  Linux平台RTMP|RTSP直播播放SDK;
  •  Android平台RTMP直播推送SDK(采集Unity窗体、摄像头、麦克风或Unity声音);
  •  Android平台轻量级RTSP服务SDK(采集Unity窗体、摄像头、麦克风或Unity声音);
  •  Android平台RTMP|RTSP直播播放SDK;
  •  iOS平台RTMP|RTSP直播播放SDK。

大牛直播SDK Unity3D接口调用SDK说明

Unity3d RTSP/RTMP直播播放端SDK视频演示1

Unity3d RTSP/RTMP直播播放端SDK视频演示2

从播放器到协议栈,你愿意做行业里的“底座角色”吗?

一、音视频行业热闹吗?热闹。成熟吗?还早。

过去五年,音视频行业进入了“黄金阶段”:

  • 商业上,各类直播/会议/监控/可视化调度平台如雨后春笋;
  • 技术上,从H.264到H.265,从RTMP到WebRTC,从私有协议到GB28181、SIP;
  • 市场上,从ToC的短视频/直播电商,到ToB的安防、政务、工业物联全覆盖;

但在热闹的另一面,是极度分化:

  • 一边是“能播一段视频就能卖方案”的轻技术派;
  • 一边是“没有完整链路就根本无法上线”的系统构建者;

大牛直播SDK,一直属于后者。


二、我们在写的不是SDK,而是系统跑得起来的底层基座

从2015年至今,大牛直播SDK坚持全自研内核,覆盖以下核心模块:

  • RTMP推流内核(跨平台):H.264/H.265软硬编支持、自适应网络、断线重连;
  • RTSP/RTMP播放器内核:低延迟、首屏秒开、软硬解切换、多实例播放,延迟低至100~300ms;
  • 轻量级RTSP服务端模块:Android/iOS/Windows/Linux均可部署,支持屏幕/摄像头实时推送;
  • 多路转发网关模块:RTSP转RTMP、RTMP转GB28181、RTSP转RTSP,链路自由组合;
  • 录像SDK:支持推流端/播放端/转发链路录制,分片管理、支持回调;
  • GB28181设备接入SDK(含Android):SIP信令、心跳/目录/媒体/位置订阅,PS封装上传;

这些模块,串起来就是一整套可以在政企、工业、安防、教育、能源等领域中落地部署的完整系统级能力。不是“调用API”,而是搭出业务链路、撑起核心服务的能力单元


三、大牛直播SDK的定位是什么?不是“音视频组件”,是“业务闭环中的通信引擎”

在行业中,大多数SDK是“功能型的”:负责推流、拉流、播放、截图、录制这些“点式能力”。

而大牛直播SDK的设计原则是“系统型的”:关注场景、链路、延迟、稳定性、可控性、兼容性和可扩展性

我们不仅仅“实现功能”,而是把功能放进系统中,确保它长时间、稳定、安全地运行


四、我们的客户,不再局限于“需要播放器”的群体,而是“要让全系统跑起来”的工程师

编辑

我们接触的客户类型包括但不限于:

领域 典型需求 我们的角色
? 政务可视化平台 多路RTSP/RTMP输入、统一上屏、延迟控制、异常反馈 提供播放+转发+控制模块
? 执法终端系统 Android设备GB28181接入、摄像头推流、边录边播 提供GB28181+推流+录像组件
? AI识别/车路协同 视频流输入、实时解码为YUV/RGB送AI推理 播放器输出+解码后数据回调
? 工业监控/边缘服务器 Linux系统RTSP接入、录像/转发/断点恢复 播放+录像+流控整合模块
? 软硬一体产品厂商 内置RTSP服务端、屏幕推送、后台服务流转 提供服务端模块和界面对接能力

他们不是“玩玩直播”,而是在做真正跑在政府、能源、工厂、军警终端上的长期部署系统


五、我们的产品哲学:不是能跑Demo,而是能撑住系统

很多SDK能在演示里“秒开”,但上线后一夜崩溃;
很多系统能在局域网跑通,一出公网全断链;
很多播放器能显示画面,却无法拉出YUV回调或做异常恢复。

而大牛直播SDK关注的是:

  • ✅ 多路并发播放是否会崩溃?
  • ✅ 弱网中推流能否自动重连、调整码率?
  • ✅ 播放器是否支持事件回调、流重定向、状态统计?
  • ✅ 录像是否有切片机制?录制是否能对接前端UI和后端存储?
  • ✅ RTSP服务是否能轻量嵌入、支持标准播放?

我们在乎的不只是“功能覆盖”,而是“工程闭环”。


六、行业里,我们不是最大的,但我们一直是最“硬核”的那一拨人

我们不追流量,不讲融资,不打概念。
我们写的是 跑得动、抗得住、可组合的 SDK 内核

我们不是那种“热闹一下”的音视频公司。我们在客户部署现场、技术负责人手里、嵌入终端设备中,用系统稳定性赢得长期合作。


七、结语:行业需要潮水,也需要堤坝

音视频行业的“风口”永远在换,从直播带货到会议SaaS,从元宇宙到WebRTC,但底层支撑这切换的,是一套套稳定的 编码、传输、播放、录制、协议适配能力

而这,正是大牛直播SDK一直坚持在写的东西。

我们不是定义行业的人,但我们愿意为真正跑得起来的系统,提供底层引擎。


? SDK模块试用:大牛直播SDK
? 更多技术实践博客:音视频牛哥-CSDN博客

HTTP-FLV直播播放SDK

技术特点和优势:

Android平台HTTP/HTTPS-FLV直播播放SDK,全自研内核,功能齐全、性能强劲、毫秒级延迟超低(150-300ms),资源占用少,HTTP/HTTPS-FLV直播播放器标杆产品。

功能支持

  •  [支持播放协议]HTTP/HTTPS;
  •  [多实例播放]支持多实例播放;
  •  [事件回调]支持网络状态、buffer状态等回调;
  •  [视频格式]H.264、H.265;
  •  [音频格式]AAC、PCMA、PCMU;
  •  [软解码]支持H.264/H.265软解;
  •  [硬解码]支持特定机型H.264/H.265硬解,支持设置Surface模式硬解和普通模式硬解码;
  •  [缓冲时间设置]支持buffer time设置;
  •  [首屏秒开]支持首屏秒开模式;
  •  [低延迟模式]支持低延迟模式设置(公网150~300ms);
  •  [复杂网络处理]支持断网重连等各种网络环境自动适配;
  •  [音视频多种render机制]Android平台,视频:SurfaceView/GLSurfaceView,音频:AudioTrack/OpenSL ES;
  •  [实时静音]支持播放过程中,实时静音/取消静音;
  •  [实时音量调节]支持播放过程中实时调节音量;
  •  [实时快照]支持播放过程中截取当前播放画面;
  •  [渲染角度]支持0°,90°,180°和270°四个视频画面渲染角度设置;
  •  [渲染镜像]支持水平反转、垂直反转模式设置;
  •  [等比例缩放]支持图像等比例缩放绘制(Android设置surface模式硬解模式不支持);
  •  [实时下载速度更新]支持当前下载速度实时回调(支持设置回调时间间隔);
  •  [解码前视频数据回调]支持H.264/H.265数据回调;
  •  [解码后视频数据回调]支持解码后YUV/RGB数据回调;
  •  [解码前音频数据回调]支持AAC/PCMA/PCMU数据回调;
  •  [音视频自适应]支持播放过程中,音视频信息改变后自适应;
  •  [扩展录像功能]完美支持和录像SDK组合使用,录像相关功能,可参见”Windows/Android/iOS录像SDK“。

对应Demo

  •  Android工程:SmartPlayerV2;

技术对接文档

相关博客

iOS平台RTSP|RTMP直播播放器技术接入说明

 技术背景

大牛直播SDK自2015年发布RTSP、RTMP直播播放模块,迭代从未停止,SmartPlayer功能强大、性能强劲、高稳定、超低延迟、超低资源占用。无需赘述,全自研内核,行业内一致认可的跨平台RTSP、RTMP直播播放器。本文以iOS平台为例,介绍下如何集成RTSP、RTMP播放模块。

技术对接

 系统要求

  • SDK支持iOS 9.0及以上版本;
  • 支持的CPU架构:arm64(真机调试)。

准备工作

  • 相关库:libSmartPlayerSDK.a
  • 相关头文件:
    1. nt_common_media_define.h(如需转发或第三方数据对接)
    2. nt_event_define.h
    3. SmartPlayerSDK.h
  • 如集需要引入的framework
    1. libbz.tbd
    2. Libbz2.tbd
    3. libiconv.tbd
    4. libstdc++.tbd
    5. Libc++.tbd
    6. Accelerate.framework
    7. AssetsLibrary.framework
    8. AudioToolBox.framework
    9. AVFoundation.framework
    10. CoreMedia.framework
    11. Foundation.framework
    12. GLKit.framework
    13. OpenGLES.framework
    14. UIKit.framework
    15. VideoToolBox.framework
  • 如需集成到自己系统测试,请用大牛直播SDK的app name:

Info.plist–>右键Open As–>Source Code

添加或者编辑

<key>CFBundleName</key>
<string>SmartiOSPlayer</string>

  • 快照添加到“照片”权限:

Info.plist–>右键Open As–>Source Code 添加:

<key>NSPhotoLibraryUsageDescription</key>
<string>1</string>

  • 如需后台播放音频(添加后台播放权限):

功能支持

iOS端,RTMP|RTSP直播播放,我们设计实现的功能如下:

  • 音频:AAC/PCMA/PCMU/SPEEX(RTMP);
  • 视频:H.264;
  • 播放协议:RTMP或RTSP;
  • 支持纯音频、纯视频、音视频播放;
  • 支持多实例播放;
  • 支持网络状态、buffer状态等回调;
  • [RTSP协议]支持RTSP TCP/UDP模式设置;
  • [RTSP协议]支持RTSP TCP、UDP模式自动切换;
  • [RTSP协议]支持RTSP超时时间设置,单位:秒;
  • [RTSP协议]支持上报RTSP 401事件,如URL携带鉴权信息,会自动处理;
  • 支持buffer time设置;
  • 支持实时静音、取消静音;
  • 支持首屏秒开功能(需服务器缓存GOP);
  • 支持超低延迟模式; 断网自动重连,支持视频追赶;
  • 支持视频view实时旋转(0° 90° 180° 270°);
  • 支持视频view水平反转、垂直反转;
  • 支持图像等比例缩放绘制;
  • 支持实时快照;
  • 支持实时音量调节;
  • 支持YUV数据回调;
  • 支持H.264|H.265数据回调;
  • 支持AAC/SPEEX/PCMA/PCMU数据回调;
  • 支持RTMP扩展H.265播放(Enhanced RTMP);
  • 支持扩展录像功能;
  • 支持Unity3D接口;
  • 支持H.264扩展SEI接收模块;
  • 支持iOS 9.0及以上版本。

播放模块接口详解

iOS播放端SDK接口详解

调用描述 接口 接口描述
最先调用,创建播放实例,如成功返回player实例 SmartPlayerInitPlayer 初始化,创建player实例,此接口请第一个调用
Event回调 SmartPlayerDelegate 设置event callback,上层由handleSmartPlayerEvent处理
软、硬解码设置 SmartPlayerSetVideoDecoderMode 设置是否用硬解码播放,如硬解码不支持,自动适配到软解码

0: 软解码;

 1: 硬解码.

创建播放view SmartPlayerCreatePlayView x y width height 指定播放位置
设置播放view SmartPlayerSetPlayView 设置播放view到底层SDK
释放播放view SmartPlayeReleasePlayView 释放播放view
视频回调 设置YUV回调 SmartPlayerSetYuvBlock 设置拉流时,视频YUV数据回调
YUV回调 PlayerYuvDataBlock 提供解码后YUV/RGB数据接口,供用户自己render或进一步处理(如视频分析)
播放模式 缓冲时间设置 SmartPlayerSetBuffer 设置播放端缓存数据buffer,单位:毫秒,如不需buffer,设置为0
首屏秒开 SmartPlayerSetFastStartup 设置快速启动后,如果CDN缓存GOP,实现首屏秒开
低延迟模式 SmartPlayerSetLowLatencyMode 针对类似于直播娃娃机等期待超低延迟的使用场景,超低延迟播放模式下,延迟可达到200~400ms
快速切换URL SmartPlayerSwitchPlaybackUrl 快速切换播放url,快速切换时,只换播放source部分,适用于不同数据流之间,快速切换
RTSP TCP/UDP模式设置 SmartPlayerSetRTSPTcpMode 设置RTSP TCP/UDP模式,如不设置,默认UDP模式
RTSP超时时间设置 SmartPlayerSetRTSPTimeout 设置RTSP超时时间,timeout单位为秒,必须大于0
设置RTSP TCP/UDP自动切换 SmartPlayerSetRTSPAutoSwitchTcpUdp 对于RTSP来说,有些可能支持rtp over udp方式,有些可能支持使用rtp over tcp方式

为了方便使用,有些场景下可以开启自动尝试切换开关, 打开后如果udp无法播放,sdk会自动尝试tcp, 如果tcp方式播放不了,sdk会自动尝试udp.

实时静音 SmartPlayerSetMute 实时静音
设置播放音量 SmartPlayerSetAudioVolume 播放端音量实时调节,范围[0,100],0时为静音,100为原始流数据最大音量
视频镜像旋转 旋转 SmartPlayerSetRotation 设置顺时针旋转, 注意除了0度之外, 其他角度都会额外消耗性能,当前支持 0度,90度, 180度, 270度 旋转
水平反转 SmartPlayerSetFlipHorizontal 设置视频水平反转
垂直反转 SmartPlayerSetFlipVertical 设置视频垂直反转
设置URL SmartPlayerSetPlayURL 设置播放或录像的url
开始播放 SmartPlayerStart 开始播放RTSP/RTMP流
停止播放 SmartPlayerStop 停止播放RTSP/RTMP流
销毁播放实例 SmartPlayerUnInitPlayer 结束时必须调用close接口释放资源

录像模块接口详解

如需录像,录像相关的接口如下:

iOS播放端录像SDK接口详解

调用描述 接口 接口描述
录像设置 设置录像目录 SmartPlayerSetRecorderDirectory 设置录像文件目录
设置录像文件大小 SmartPlayerSetRecorderFileMaxSize 设置每个录像文件的大小,比如100M,超过这个大小后,会自动生成下一个录像文件
音频转码 SmartPlayerSetRecorderAudioTranscodeAAC 设置录像时音频转AAC编码的开关

aac比较通用,sdk增加其他音频编码(比如speex, pcmu, pcma等)转aac的功能.

录制视频 SmartPlayerSetRecorderVideo 设置是否录视频,默认的话,如果视频源有视频就录,没有就不录, 但有些场景下可能不想录制视频,只想录音频,所以增加个开关
录制音频 SmartPlayerSetRecorderAudio 设置是否录音频,默认的话,如果视频源有音频就录,没有就不录, 但有些场景下可能不想录制音频,只想录视频,所以增加个开关
开始录像 SmartPlayerStartRecorder 开始录像
停止录像 SmartPlayerStopRecorder 停止录像

Event回调详解

由于iOS播放录像SDK和播放端SDK可组合使用,相关Event同步更新在iOS播放端SDK(如下图):

iOS播放端SDK Event回调说明

事件ID 事件描述
EVENT_DANIULIVE_ERC_PLAYER_STARTED 开始播放
EVENT_DANIULIVE_ERC_PLAYER_CONNECTING 播放端连接中
EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED 播放端连接失败
EVENT_DANIULIVE_ERC_PLAYER_CONNECTED 播放端连接成功
EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED 播放端连接断开
EVENT_DANIULIVE_ERC_PLAYER_STOP 停止播放
EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO 返回视频宽、高信息
EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED 收不到媒体数据(可能是URL错误)
EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL 快速切换URL
EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE 开始一个新的录像文件(param3返回包含录像路径在内的录像文件名)
EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED 已生成一个录像文件(param3返回包含录像路径在内的录像文件名)
EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE 播放端实时快照
EVENT_DANIULIVE_ERC_PLAYER_START_BUFFERING 开始缓冲数据
EVENT_DANIULIVE_ERC_PLAYER_BUFFERING 缓冲中(param1参数

会返回缓冲百分比)

EVENT_DANIULIVE_ERC_PLAYER_STOP_BUFFERING 停止缓冲数据
EVENT_DANIULIVE_ERC_PLAYER_DOWNLOAD_SPEED 返回当前RTSP/RTMP流实时下载速度
EVENT_DANIULIVE_ERC_PLAYER_RTSP_STATUS_CODE RTSP收到错误码,可能是用户名、密码不对

逻辑调用

先说开始播放:

//
//  ViewController.m
//  SmartiOSPlayerV2
//
//  Author: daniusdk.com
//  WeChat: xinsheng120
//  Created by daniulive on 2016/01/03.
//
- (void)playBtn:(UIButton *)button {
    
    NSLog(@"playBtn only++");
    
    button.selected = !button.selected;
    
    if (button.selected)
    {
        if(is_playing_)
            return;
        
        [self InitPlayer];
        
        //如需处理回调的用户数据+++++++++
        __weak __typeof(self) weakSelf = self;
        
        _smart_player_sdk.spUserDataCallBack = ^(int data_type, unsigned char *data, unsigned int size, unsigned long long timestamp, unsigned long long reserve1, long long reserve2, unsigned char *reserve3)
        {
            [weakSelf OnUserDataCallBack:data_type data:data size:size timestamp:timestamp reserve1:reserve1 reserve2:reserve2 reserve3:reserve3];
        };
        
        Boolean enableUserDataCallback = YES;
        [_smart_player_sdk SmartPlayerSetUserDataCallback:enableUserDataCallback];
         //如需处理回调的用户数据---------
        
        if(![self StartPlayer])
        {
            NSLog(@"Call StartPlayer failed..");
        }
        
        [playbackButton setTitle:@"停止播放" forState:UIControlStateNormal];
        
        is_playing_ = YES;
    }
    else
    {
        if ( !is_playing_ )
            return;
        
        [self StopPlayer];
        
        if(!is_recording_)
        {
            [self UnInitPlayer];
        }
        
        [playbackButton setTitle:@"开始播放" forState:UIControlStateNormal];
        
        is_mute_ = NO;
        [muteButton setTitle:@"实时静音" forState:UIControlStateNormal];
        
        is_playing_ = NO;
    }
}

其中,InitPlayer实现如下:

-(bool)InitPlayer
{
    NSLog(@"InitPlayer++");
    
    if(is_inited_player_)
    {
        NSLog(@"InitPlayer: has inited before..");
        return true;
    }
    
    //NSString* in_cid = @"";
    //NSString* in_key = @"";
    
    //[SmartPlayerSDK SmartPlayerSetSDKClientKey:in_cid in_key:in_key reserve1:0 reserve2:nil];
    
    _smart_player_sdk = [[SmartPlayerSDK alloc] init];
    
    if (_smart_player_sdk ==nil ) {
        NSLog(@"SmartPlayerSDK init failed..");
        return false;
    }
    
    if (playback_url_.length == 0) {
        NSLog(@"playback url is nil..");
        return false;
    }
    
    if (_smart_player_sdk.delegate == nil)
    {
        _smart_player_sdk.delegate = self;
        NSLog(@"SmartPlayerSDK _player.delegate:%@", _smart_player_sdk);
    }
    
    NSInteger initRet = [_smart_player_sdk SmartPlayerInitPlayer];
    if ( initRet != DANIULIVE_RETURN_OK )
    {
        NSLog(@"SmartPlayerSDK call SmartPlayerInitPlayer failed, ret=%ld", (long)initRet);
        return false;
    }
    
    [_smart_player_sdk SmartPlayerSetPlayURL:playback_url_];
    //[self try_set_rtsp_url:playback_url_];
    
    //超低延迟模式设置
    [_smart_player_sdk SmartPlayerSetLowLatencyMode:(NSInteger)is_low_latency_mode_];
    
    //buffer time设置
    if(buffer_time_ >= 0)
    {
        [_smart_player_sdk SmartPlayerSetBuffer:buffer_time_];
    }
    
    //快速启动模式设置
    [_smart_player_sdk SmartPlayerSetFastStartup:(NSInteger)is_fast_startup_];
    
    NSLog(@"[SmartPlayerV2]is_fast_startup_:%d, buffer_time_:%ld", is_fast_startup_, (long)buffer_time_);
    
    //RTSP TCP还是UDP模式
    [_smart_player_sdk SmartPlayerSetRTSPTcpMode:is_rtsp_tcp_mode_];
 
    //设置RTSP超时时间
    NSInteger rtsp_timeout = 10;
    [_smart_player_sdk SmartPlayerSetRTSPTimeout:rtsp_timeout];
    
    //设置RTSP TCP/UDP自动切换
    NSInteger is_tcp_udp_auto_switch = 1;
    [_smart_player_sdk SmartPlayerSetRTSPAutoSwitchTcpUdp:is_tcp_udp_auto_switch];
    
    //快照设置 如需快照 参数传1
    [_smart_player_sdk SmartPlayerSaveImageFlag:save_image_flag_];
    
    //如需查看实时流量信息,可打开以下接口
    NSInteger is_report = 1;
    NSInteger report_interval = 3;
    [_smart_player_sdk SmartPlayerSetReportDownloadSpeed:is_report report_interval:report_interval];
    
    //录像端音频,是否转AAC后保存
    NSInteger is_transcode = 1;
    [_smart_player_sdk SmartPlayerSetRecorderAudioTranscodeAAC:is_transcode];
    
    //录制MP4文件 是否录制视频
    NSInteger is_record_video = 1;
    [_smart_player_sdk SmartPlayerSetRecorderVideo:is_record_video];
    
    //录制MP4文件 是否录制音频
    NSInteger is_record_audio = 1;
    [_smart_player_sdk SmartPlayerSetRecorderAudio:is_record_audio];
    
    
    is_inited_player_ = YES;
    
    NSLog(@"InitPlayer--");
    return true;
}

停止播放StopPlayer实现如下:

-(bool)StopPlayer
{
    NSLog(@"StopPlayer++");
    
    if (_smart_player_sdk != nil)
    {
        [_smart_player_sdk SmartPlayerStop];
    }
    
    if (!is_audio_only_) {
        if (_glView != nil) {
            [_glView removeFromSuperview];
            [SmartPlayerSDK SmartPlayeReleasePlayView:(__bridge void *)(_glView)];
            _glView = nil;
        }
    }
    
    NSLog(@"StopPlayer--");
    return true;
}

UnInitPlayer实现如下:

-(bool)UnInitPlayer
{
    NSLog(@"UnInitPlayer++");
    
    if (_smart_player_sdk != nil)
    {
        [_smart_player_sdk SmartPlayerUnInitPlayer];
        
        if (_smart_player_sdk.delegate != nil)
        {
            _smart_player_sdk.delegate = nil;
        }
        
        _smart_player_sdk = nil;
    }
    
    is_inited_player_ = NO;
    
    NSLog(@"UnInitPlayer--");
    return true;
}

实时录像:

- (void)RecorderBtn:(UIButton *)button {
    
    NSLog(@"record Stream only++");
    
    button.selected = !button.selected;
    
    if (button.selected)
    {
        if(is_recording_)
            return;
        
        [self InitPlayer];
        
        //设置录像目录
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *recorderDir = [paths objectAtIndex:0];
        
        if([_smart_player_sdk SmartPlayerSetRecorderDirectory:recorderDir] != DANIULIVE_RETURN_OK)
        {
            NSLog(@"Call SmartPlayerSetRecorderDirectory failed..");
        }
        
        //每个录像文件大小
        NSInteger size = 200;
        if([_smart_player_sdk SmartPlayerSetRecorderFileMaxSize:size] != DANIULIVE_RETURN_OK)
        {
            NSLog(@"Call SmartPlayerSetRecorderFileMaxSize failed..");
        }
        
        [_smart_player_sdk SmartPlayerStartRecorder];
        [recButton setTitle:@"停止录像" forState:UIControlStateNormal];
        
        is_recording_ = YES;
    }
    else
    {
        [_smart_player_sdk SmartPlayerStopRecorder];
        [recButton setTitle:@"开始录像" forState:UIControlStateNormal];
        
        if(!is_playing_)
        {
            [self UnInitPlayer];
        }
        
        is_recording_ = NO;
    }
}

实时快照:

- (void)SaveImageBtn:(UIButton *)button {
    if ( _smart_player_sdk != nil )
    {
        //设置快照目录
        NSLog(@"[SaveImageBtn] path++");
        
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *saveImageDir = [paths objectAtIndex:0];
        
        NSLog(@"[SaveImageBtn] path: %@", saveImageDir);
        
        NSString* symbol = @"/";
        
        NSString* png = @".png";
        
        // 1.创建时间
        NSDate *datenow = [NSDate date];
        // 2.创建时间格式化
        NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
        // 3.指定格式
        formatter.dateFormat = @"yyyyMMdd_HHmmss";
        // 4.格式化时间
        NSString *timeSp = [formatter stringFromDate:datenow];
        
        NSString* image_name =  [saveImageDir stringByAppendingString:symbol];
        
        image_name = [image_name stringByAppendingString:timeSp];
        
        image_name = [image_name stringByAppendingString:png];
        
        NSLog(@"[SaveImageBtn] image_name: %@", image_name);
        
        [_smart_player_sdk SmartPlayerSaveCurImage:image_name];
    }
}

Event回调处理如下:

- (NSInteger) handleSmartPlayerEvent:(NSInteger)nID param1:(unsigned long long)param1 param2:(unsigned long long)param2 param3:(NSString*)param3 param4:(NSString*)param4 pObj:(void *)pObj;
{
    NSString* player_event = @"";
    NSString* lable = @"";
    
    if (nID == EVENT_DANIULIVE_ERC_PLAYER_STARTED) {
        player_event = @"[event]开始播放..";
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_CONNECTING)
    {
        player_event = @"[event]连接中..";
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED)
    {
        player_event = @"[event]连接失败..";
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_CONNECTED)
    {
        player_event = @"[event]已连接..";
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED)
    {
        player_event = @"[event]断开连接..";
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_STOP)
    {
        player_event = @"[event]停止播放..";
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO)
    {
        NSString *str_w = [NSString stringWithFormat:@"%ld", (long)param1];
        NSString *str_h = [NSString stringWithFormat:@"%ld", (long)param2];
        
        lable = @"[event]视频解码分辨率信息: ";
        player_event = [lable stringByAppendingFormat:@"%@*%@", str_w, str_h];
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED)
    {
        player_event = @"[event]收不到RTMP数据..";
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL)
    {
        player_event = @"[event]快速切换url..";
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE)
    {
        if ((int)param1 == 0)
        {
            NSLog(@"[event]快照成功: %@", param3);
            lable = @"[event]快照成功:";
            player_event = [lable stringByAppendingFormat:@"%@", param3];
            
            tmp_path_ = param3;
            
            image_path_ = [ UIImage imageNamed:param3];
            
            UIImageWriteToSavedPhotosAlbum(image_path_, self, @selector(image:didFinishSavingWithError:contextInfo:), NULL);
        }
        else
        {
            lable = @"[event]快照失败";
            player_event = [lable stringByAppendingFormat:@"%@", param3];
        }
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE)
    {
        lable = @"[event]录像写入新文件..文件名:";
        player_event = [lable stringByAppendingFormat:@"%@", param3];
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED)
    {
        lable = @"一个录像文件完成..文件名:";
        player_event = [lable stringByAppendingFormat:@"%@", param3];
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_START_BUFFERING)
    {
        //NSLog(@"[event]开始buffer..");
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_BUFFERING)
    {
        NSLog(@"[event]buffer百分比: %lld", param1);
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_STOP_BUFFERING)
    {
        //NSLog(@"[event]停止buffer..");
    }
    else if (nID == EVENT_DANIULIVE_ERC_PLAYER_DOWNLOAD_SPEED)
    {
        NSInteger speed_kbps = (NSInteger)param1*8/1000;
        NSInteger speed_KBs = (NSInteger)param1/1024;
        
        lable = @"[event]download speed :";
        player_event = [lable stringByAppendingFormat:@"%ld kbps - %ld KB/s", (long)speed_kbps, (long)speed_KBs];
    }
    else if(nID == EVENT_DANIULIVE_ERC_PLAYER_RTSP_STATUS_CODE)
    {
        lable = @"[event]RTSP status code received:";
        player_event = [lable stringByAppendingFormat:@"%ld", (long)param1];
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_async(dispatch_get_main_queue(), ^{
                UIAlertController *aleView=[UIAlertController alertControllerWithTitle:@"RTSP错误状态" message:player_event preferredStyle:UIAlertControllerStyleAlert];
                UIAlertAction *action_ok=[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:nil];
                [aleView addAction:action_ok];
                
                [self presentViewController:aleView animated:YES completion:nil];
            });
        });
    }
    else if(nID == EVENT_DANIULIVE_ERC_PLAYER_NEED_KEY)
    {
        player_event = @"[event]RTMP加密流,请设置播放需要的Key..";
    }
    else if(nID == EVENT_DANIULIVE_ERC_PLAYER_KEY_ERROR)
    {
        player_event = @"[event]RTMP加密流,Key错误,请重新设置..";
    }
    else
        NSLog(@"[event]nID:%lx", (long)nID);
    
    NSString* player_event_tag = @"当前状态:";
    NSString* event = [player_event_tag stringByAppendingFormat:@"%@", player_event];
    
    if ( player_event.length != 0)
    {
        NSLog(@"%@", event);
    }
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_async(dispatch_get_main_queue(), ^{
                self.textPlayerEventLabel.text = event;
            });
    });
    
    return 0;
}

总结

iOS平台RTSP、RTMP直播播放模块,延迟低、资源占有少,性能优异。由于设备和系统比较单一,优先考虑硬解码,除了基础播放外,我们还实现了实时快照、实时录像、实时回调YUV数据、实时音量调节等,实际体验下来,iOS平台RTMP和RTSP,可以轻松毫秒级。

Android平台实现屏幕录制(屏幕投影)|音频播放采集|麦克风采集并推送RTMP或轻量级RTSP服务

技术背景

好多开发者,希望我们能系统的介绍下无纸化同屏的原理和集成步骤,以Android平台为例,无纸化同屏将Android设备上的屏幕内容实时投射到另一个显示设备(如Windows终端、国产化操作系统或另一台Android设备)上,从而实现多屏互动和内容的无缝共享。

技术考量指标

本文以大牛直播SDK Android同屏采集推送为例,介绍下我们前些年做Android同屏采集推送的时候,一些注意点:

  1. 声明所需权限:在Android应用的AndroidManifest.xml文件中声明必要的权限;
  2. 获取MediaProjectionManager服务:在你Activity或Service,通过getSystemService方法获取MediaProjectionManager服务;
  3. 创建并启动屏幕捕获Intent:使用MediaProjectionManager的createScreenCaptureIntent方法创建一个Intent,该Intent会启动一个系统对话框,请求用户授权屏幕捕获;
  4. 处理用户授权结果:在onActivityResult回调中,根据用户授权的结果来获取MediaProjection对象;
  5. 创建VirtualDisplay并捕获屏幕:获得了MediaProjection对象,就可以使用它来创建一个VirtualDisplay,这个VirtualDisplay会捕获屏幕内容并将其发送到指定的Surface;
  6. 资源释放:当屏幕捕获不再需要时,确保释放MediaProjection和VirtualDisplay对象,以避免资源泄露;
  7. 视频编码:通过上述步骤,捕获带的屏幕内容需要进行视频编码,以便在网络中传输。如H.264、H.265等,以及设置合适的分辨率、帧率、码率,以适应不同的网络环境和接收设备的性能;
  8. 流媒体协议:为了将编码后的视频流实时传输到接收端,Android无纸化同屏技术通常采用RTMP推流模式或轻量级RTSP服务。

技术实现

本文以大牛直播SDK的Android的SmartServicePublisherV2的同屏demo为例,Android采集计时器,编码打包分别启动RTMP推送和轻量级RTSP服务,Windows过来分别拉取RTMP和RTSP的流,整体延迟毫秒级:

启动APP后,先选择需要采集的分辨率(如果选原始分辨率,系统不做缩放),然后选择“启动媒体投影”,并分别启动音频播放采集、采集麦克风。如果音频播放采集和采集麦克风都打开,可以通过右侧下拉框,推送过程中,音频播放采集和麦克风采集实时切换。需要注意的是,Android采集音频播放的audio,音频播放采集是依赖屏幕投影的,屏幕投影关闭后,音频播放也就采不到了。

编码的话,考虑到屏幕分辨率一般不会太低,我们可以缩放后再推送,默认我们开启了原始分辨率、标准分辨率、低分辨率选项设置。一般建议标准分辨率即可。如果对画质和分辨率要求比较高,可以选择原始分辨率。设备支持硬编码,优先选择H.264硬编,如果是H.265硬编,需要RTMP服务器支持扩展H.265(或Enhanced RTMP)。

都选择好后,设置RTMP推送的URL,点开始RTMP推送按钮即可。

如果需要通过轻量级RTSP服务,发布RTSP流,先点击启动RTSP服务按钮,RTSP服务启动后,再点击启动RTSP流,RTSP流发布成功后,界面会回调上来RTSP拉流的URL。

下面从代码逻辑实现角度,介绍下同屏的具体流程:

启动媒体服务,进入系统后,我们会自动启动媒体服务,对应的实现逻辑如下:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private void start_media_service() {
	Intent intent = new Intent(getApplicationContext(), StreamMediaDemoService.class);
	if (Build.VERSION.SDK_INT >= 26) {
		Log.i(TAG, "startForegroundService");
		startForegroundService(intent);
	} else
		startService(intent);

	bindService(intent, service_connection_, Context.BIND_AUTO_CREATE);
	button_stop_media_service_.setText("停止媒体服务");
}

private void stop_media_service() {
	if (media_engine_callback_ != null)
		media_engine_callback_.reset(null);

	if (media_engine_ != null) {
		media_engine_.unregister_callback(media_engine_callback_);
		media_engine_ = null;
	}

	media_engine_callback_ = null;

	if (media_binder_ != null) {
		media_binder_ = null;
		unbindService(service_connection_);
	}

	Intent intent = new Intent(getApplicationContext(), StreamMediaDemoService.class);
	stopService(intent);
	button_stop_media_service_.setText("启动媒体服务");
}

需要注意的是,Android 6.0及以上版本,动态获取Audio权限:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private boolean check_record_audio_permission() {
	//6.0及以上版本,动态获取Audio权限
	if (PackageManager.PERMISSION_GRANTED == checkPermission(android.Manifest.permission.RECORD_AUDIO, Process.myPid(), Process.myUid()))
		return true;

	return false;
}

private void request_audio_permission() {
	if (Build.VERSION.SDK_INT < 23)
		return;

	Log.i(TAG, "requestPermissions RECORD_AUDIO");
	ActivityCompat.requestPermissions(this, new String[] {android.Manifest.permission.RECORD_AUDIO}, REQUEST_AUDIO_CODE);
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
	switch(requestCode){
		case REQUEST_AUDIO_CODE:
			if (grantResults != null && grantResults.length > 0 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
				Log.i(TAG, "RECORD_AUDIO permission has been granted");
			}else {
				Toast.makeText(this, "请开启录音权限!", Toast.LENGTH_SHORT).show();
			}
			break;
	}
}

启动、停止媒体投影:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private class ButtonStartMediaProjectionListener implements OnClickListener {
	public void onClick(View v) {
		if (null == media_engine_)
			return;

		if (media_engine_.is_video_capture_running()) {
			media_engine_.stop_audio_playback_capture();
			media_engine_.stop_video_capture();
			resolution_selector_.setEnabled(true);
			button_capture_audio_playback_.setText("采集音频播放");
			button_start_media_projection_.setText("启动媒体投影");
			return;
		}

		Intent capture_intent;
		capture_intent = media_projection_manager_.createScreenCaptureIntent();

		startActivityForResult(capture_intent, REQUEST_MEDIA_PROJECTION);
		Log.i(TAG, "startActivityForResult request media projection");
	}
}

启动媒体投影后,选择“采集音频播放”,如果需要采集麦克风,可以点击“采集麦克风”:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private class ButtonCaptureAudioPlaybackListener implements OnClickListener {
	public void onClick(View v) {
		if (null == media_engine_)
			return;

		if (media_engine_.is_audio_playback_capture_running()) {
			media_engine_.stop_audio_playback_capture();
			button_capture_audio_playback_.setText("采集音频播放");
			return;
		}

		if (!media_engine_.start_audio_playback_capture(44100, 1))
			Log.e(TAG, "start_audio_playback_capture failed");
		else
			button_capture_audio_playback_.setText("停止音频播放采集");
	}
}

private class ButtonStartAudioRecordListener implements OnClickListener {
	public void onClick(View v) {
		if (null == media_engine_)
			return;

		if (media_engine_.is_audio_record_running()) {
			media_engine_.stop_audio_record();
			button_start_audio_record_.setText("采集麦克风");
			return;
		}

		if (!media_engine_.start_audio_record(44100, 1))
			Log.e(TAG, "start_audio_record failed");
		else
			button_start_audio_record_.setText("停止麦克风");
	}
}

启动、停止RTMP推送:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private class ButtonRTMPPublisherListener implements OnClickListener {
	@Override
	public void onClick(View v) {
		if (null == media_engine_)
			return;

		if (media_engine_.is_rtmp_stream_running()) {
			media_engine_.stop_rtmp_stream();
			button_rtmp_publisher_.setText("开始RTMP推送");
			text_view_rtmp_url_.setText("RTMP URL: ");
			Log.i(TAG, "stop rtmp stream");
			return;
		}

		if (!media_engine_.is_video_capture_running())
			return;

		String rtmp_url;
		if (input_rtmp_url_ != null && input_rtmp_url_.length() > 1) {
			rtmp_url = input_rtmp_url_;
			Log.i(TAG, "start, input rtmp url:" + rtmp_url);
		} else {
			rtmp_url = baseURL + String.valueOf((int) (System.currentTimeMillis() % 1000000));
			Log.i(TAG, "start, generate random url:" + rtmp_url);
		}

		media_engine_.set_fps(fps_);
		media_engine_.set_gop(gop_);
		media_engine_.set_video_encoder_type(video_encoder_type);

		if (!media_engine_.start_rtmp_stream(rtmp_url))
			return;

		button_rtmp_publisher_.setText("停止RTMP推送");
		text_view_rtmp_url_.setText("RTMP URL:" + rtmp_url);
		Log.i(TAG, "RTMP URL:" + rtmp_url);
	}
}

启动RTSP服务:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private class ButtonRTSPServiceListener implements OnClickListener {
	public void onClick(View v) {
		if (null == media_engine_)
			return;

		if (media_engine_.is_rtsp_server_running()) {
			media_engine_.stop_rtsp_stream();
			media_engine_.stop_rtsp_server();
			button_rtsp_publisher_.setText("启动RTSP流");
			button_rtsp_service_.setText("启动RTSP服务");
			text_view_rtsp_url_.setText("RTSP URL:");
			return;
		}

		if (!media_engine_.start_rtsp_server(rtsp_port_, null, null))
			return;

		button_rtsp_service_.setText("停止RTSP服务");
	}
}

发布RTSP流:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
private class ButtonRtspPublisherListener implements OnClickListener {
	public void onClick(View v) {
		if (null == media_engine_)
			return;

		if (media_engine_.is_rtsp_stream_running()) {
			media_engine_.stop_rtsp_stream();
			button_rtsp_publisher_.setText("启动RTSP流");
			text_view_rtsp_url_.setText("RTSP URL:");
			return;
		}

		if (!media_engine_.is_video_capture_running())
			return;

		media_engine_.set_fps(fps_);
		media_engine_.set_gop(gop_);
		media_engine_.set_video_encoder_type(video_encoder_type);

		if (!media_engine_.start_rtsp_stream("stream1"))
			return;

		button_rtsp_publisher_.setText("停止RTSP流");
	}
}

RTSP流发布成功后,底层会把RTSP拉流的URL回调上来:

/*
 * MainActivity.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
@Override
public void on_nt_rtsp_stream_url(String url) {
	Log.i(TAG, "on_nt_rtsp_stream_url: " + url);

	MainActivity activity = get_activity();
	if (activity != null) {
		activity.runOnUiThread(new Runnable() {
			MainActivity activity_;
			String url_;

			@Override
			public void run() {
			   activity_.text_view_rtsp_url_.setText("RTSP URL:" + url_);
			}

			public Runnable set(MainActivity activity, String url) {
				this.activity_ = activity;
				this.url_ = url;
				return this;
			}
		}.set(activity, url));
	}
}

可以看到,上述操作,都是在MainActivity.java调用的,如果是需要做demo版本集成,只需要关注MainActivity.java的业务逻辑即可,为了便于开发者对接,我们做了接口的二次封装,除了常规的RTMP推送、轻量级RTSP服务设计外,如果需要录像,只要在MainActivity.java调用这里的接口逻辑即可,非常方便:

/*
 * NTStreamMediaEngine.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
package com.daniulive.smartpublisher;

public interface NTStreamMediaEngine {
    void register_callback(Callback callback);

    void unregister_callback(Callback callback);

    void set_resolution_level(int level);

    int get_resolution_level();

    /*
    * 启动媒体投影
     */
    boolean start_video_capture(int token_code, android.content.Intent token_data);

    boolean is_video_capture_running();

    void stop_video_capture();

    /*
    * 启动麦克风
     */
    boolean start_audio_record(int sample_rate, int channels);

    boolean is_audio_record_running();

    void stop_audio_record();

    /*
     *  Android 10及以上支持, Android10以下设备调用直接返回false
     *  需要有RECORD_AUDIO权限
     *  要开启媒体投影
     */
    boolean start_audio_playback_capture(int sample_rate, int channels);

    boolean is_audio_playback_capture_running();

    void stop_audio_playback_capture();

    /*
     * 输出的音频类型
     *  0: 不输出音频
     *  1: 输出麦克风
     *  2: 输出audio playback(Android 10及以上支持)
     */
    boolean set_audio_output_type(int type);

    int get_audio_output_type();

    void set_fps(int fps);

    void set_gop(int gop);

    boolean set_video_encoder_type(int video_encoder_type);

    int get_video_encoder_type();

    /*
    * 推送RTMP
     */
    boolean start_rtmp_stream(String url);

    boolean is_rtmp_stream_running();

    String get_rtmp_stream_url();

    void stop_rtmp_stream();

    /*
    * 启动RTSP Server, 需要设置端口,用户名和密码可选
     */
    boolean start_rtsp_server(int port, String user_name, String password);

    boolean is_rtsp_server_running();

    void stop_rtsp_server();

    /*
    * 发布RTSP流
     */
    boolean start_rtsp_stream(String stream_name);

    boolean is_rtsp_stream_running();

    String get_rtsp_stream_url();

    void stop_rtsp_stream();

    /*
    * 启动本地录像
     */
    boolean start_stream_record(String record_directory, int file_max_size);

    boolean is_stream_recording();

    void stop_stream_record();

    boolean is_stream_running();

    interface Callback {
        void on_nt_video_capture_stop();
        void on_nt_rtsp_stream_url(String url);
    }
}

如果对音视频这块相对了解的开发者,可以继续到NTStreamMediaProjectionEngineImpl.java文件,查看或修改相关的技术实现:

/*
 * NTStreamMediaProjectionEngineImpl.java
 * Created by daniusdk.com on 2017/04/19.
 * WeChat: xinsheng120
 */
package com.daniulive.smartpublisher;

import android.app.Activity;
import android.app.Application;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Point;
import android.graphics.Rect;
import android.media.Image;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.util.Log;
import android.util.Size;
import android.view.Surface;
import android.view.WindowManager;
import android.view.WindowMetrics;

import com.eventhandle.NTSmartEventCallbackV2;
import com.eventhandle.NTSmartEventID;
import com.voiceengine.NTAudioRecordV2;
import com.voiceengine.NTAudioRecordV2Callback;
import com.videoengine.NTMediaProjectionCapture;
import com.voiceengine.NTAudioPlaybackCapture;

import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;

public class NTStreamMediaProjectionEngineImpl implements AutoCloseable, NTStreamMediaEngine,
        NTVirtualDisplaySurfaceSinker.Callback, NTMediaProjectionCapture.Callback {
    private static final String TAG = "NTLogProjectionEngine";

    private static final Size DEFAULT_SIZE = new Size(1920, 1080);

    public static final int RESOLUTION_LOW = 0;
    public static final int RESOLUTION_MEDIUM = 1;
    public static final int RESOLUTION_HIGH = 2;

    private final Application application_;

    private final long image_thread_id_;
    private final long running_thread_id_;

    private final Handler image_handler_;
    private final Handler running_handler_;

    private final WindowManager window_manager_;
    private final MediaProjectionManager projection_manager_;
    private int screen_density_dpi_ = android.util.DisplayMetrics.DENSITY_DEFAULT;

    private final SmartPublisherJniV2 lib_publisher_;
    private final LibPublisherWrapper.RTSPServer rtsp_server_;
    private final LibPublisherWrapper stream_publisher_;

    private final CopyOnWriteArrayList<NTStreamMediaEngine.Callback> callbacks_ = new CopyOnWriteArrayList<>();

    private final AtomicReference<VideoSinkerCapturePair> video_capture_pair_ = new AtomicReference<>();

    private final AudioRecordCallbackImpl audio_record_callback_;
    private final AudioPlaybackCaptureCallbackImpl audio_playback_capture_callback_;

    private final AtomicReference<NTAudioRecordV2> audio_record_ = new AtomicReference<>();
    private final AtomicReference<NTAudioPlaybackCapture> audio_playback_capture_ = new AtomicReference<>();
	
	...
}

以Android平台RTMP推送模块为例,我们主要实现了如下功能:

  • 音频编码:AAC/SPEEX;
  • 视频编码:H.264、H.265;
  • 推流协议:RTMP;
  • [音视频]支持纯音频/纯视频/音视频推送;
  • [摄像头]支持采集过程中,前后摄像头实时切换;
  • 支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置;
  • 支持RTMP推送 live|record模式设置;
  • 支持前置摄像头镜像设置;
  • 支持软编码、特定机型硬编码;
  • 支持横屏、竖屏推送;
  • 支持Android屏幕采集推送;
  • 支持自建标准RTMP服务器或CDN;
  • 支持断网自动重连、网络状态回调;
  • 支持实时动态水印;
  • 支持实时快照;
  • 支持降噪处理、自动增益控制;
  • 支持外部编码前音视频数据对接;
  • 支持外部编码后音视频数据对接;
  • 支持RTMP扩展H.265(需设备支持H.265特定机型硬编码)和Enhanced RTMP;
  • 支持实时音量调节;
  • 支持扩展录像模块;
  • 支持Unity接口;
  • 支持H.264扩展SEI发送模块;
  • 支持Android 5.1及以上版本。

轻量级RTSP服务,在上述非RTMP协议依赖的基础上,增加了如下功能:

  •  [音频格式]AAC;
  •  [视频格式]H.264、H.265;
  •  [协议类型]RTSP;
  •  [传输模式]支持单播和组播模式;
  •  [端口设置]支持RTSP端口设置;
  •  [鉴权设置]支持RTSP鉴权用户名、密码设置;
  •  [获取session连接数]支持获取当前RTSP服务会话连接数;
  •  [多服务支持]支持同时创建多个内置RTSP服务;
  •  [RTSP url回调]支持设置后的rtsp url通过event回调到上层。

总结

以上是Android平台屏幕采集、音频播放声音采集、麦克风采集编码打包推送到RTMP和轻量级RTSP服务的相关技术实现,做成高稳定低延迟的同屏系统,还需要有配套好的RTMP、RTSP直播播放器,整体部署,内网大并发环境下,还需要考虑到如何组网等诸多因素。做demo容易,做个成熟的模块还是有一定的难度,以上抛砖引玉,感兴趣的开发者,可以单独跟我沟通探讨。

Android平台GB28181接入模块技术接入说明

 技术背景

今天,我们主要讲讲Android平台GB28181接入模块的技术对接,Android平台GB28181接入模块设计的目的,可实现不具备国标音视频能力的 Android终端,通过平台注册接入到现有的GB/T28181—2016服务,可用于如智能监控、智慧零售、智慧教育、远程办公、生产运输、智慧交通、车载或执法记录仪等场景。

Android终端除支持常规的音视频数据接入外,还可以支持移动设备位置(MobilePosition)订阅和通知、语音广播和语音对讲、云台控制回调和预置位查询,支持对接数据类型如下:

  • 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型);
  • 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);
  • 拉取RTSP或RTMP流并接入至GB28181平台(比如其他IPC的RTSP流,可通过Android平台GB28181接入到国标平台)。

功能支持

  •  ​[视频格式]H.264/H.265(Android H.265硬编码);
  •  [音频格式]G.711 A律、AAC;
  •  [音量调节]Android平台采集端支持实时音量调节;
  •  [H.264硬编码]支持H.264特定机型硬编码;
  •  [H.265硬编码]支持H.265特定机型硬编码;
  •  [软硬编码参数配置]支持gop间隔、帧率、bit-rate设置;
  •  [软编码参数配置]支持软编码profile、软编码速度、可变码率设置;
  • 支持纯视频、音视频PS打包传输;
  • 支持RTP OVER UDP和RTP OVER TCP被动模式;
  • 支持信令通道网络传输协议TCP/UDP设置;
  • 支持注册、注销,支持注册刷新及注册有效期设置;
  • 支持设备目录查询应答;
  • 支持心跳机制,支持心跳间隔、心跳检测次数设置;
  • 支持移动设备位置(MobilePosition)订阅和通知;
  •  适用国家标准:GB/T 28181—2016;
  • 支持语音广播;
  • 支持语音对讲;
  • 支持图像抓拍;
  • 支持历史视音频文件检索;
  • 支持历史视音频文件下载;
  • 支持历史视音频文件回放;
  • 支持云台控制和预置位查询;
  •  [实时水印]支持动态文字水印、png水印;
  •  [镜像]Android平台支持前置摄像头实时镜像功能;
  •  [实时静音]支持实时静音/取消静音;
  •  [实时快照]支持实时快照;
  •  [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
  •  [外部编码前视频数据对接]支持YUV数据对接;
  •  [外部编码前音频数据对接]支持PCM对接;
  •  [外部编码后视频数据对接]支持外部H.264数据对接;
  •  [外部编码后音频数据对接]外部AAC数据对接;
  •  [扩展录像功能]支持和录像SDK组合使用,录像相关功能。​

系统要求

  • SDK支持Android 5.1及以上版本;
  • 支持的CPU架构:armv7, arm64, x86, x86_64。

准备工作

  • 确保SmartPublisherJniV2.java放到com.daniulive.smartpublisher包名下(可在其他包名下调用);
  • 如需集成语音广播、语音对讲功能,确保SmartPlayerJniV2.java放到com.daniulive.smartplayer包名下(可在其他包名下调用);
  • smartavengine.jar和smartgbsipagent.jar加入到工程;
  • 拷贝libSmartPublisher.so和libSmartPlayer.so(如需语音广播或语音对讲)到工程;
  • AndroidManifast.xml添加相关权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" ></uses-permission>
<uses-permission android:name="android.permission.INTERNET" ></uses-permission>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>

  • Load相关so:
static {  
    System.loadLibrary("SmartPublisher");
    System.loadLibrary("SmartPlayer");
}

  • build.gradle配置32/64位库:
splits {
    abi {
        enable true
        reset()
        // Specifies a list of ABIs that Gradle should create APKs for
        include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' //select ABIs to build APKs for
        // Specify that we do not want to also generate a universal APK that includes all ABIs
        universalApk true
    }
}

  • 如需集成到自己系统测试,请用大牛直播SDK的app name,授权版按照授权app name正常使用即可;
  • 如何改app-name,strings.xml做以下修改:
<string name="app_name">SmartPublisherSDKDemo</string>

接口详解

以Android平台Camera2对接为例,信令部分需要实现如下标红接口:

public class MainActivity extends Activity implements ViewTreeObserver.OnGlobalLayoutListener, Camera2Listener,
        GBSIPAgentListener, GBSIPAgentPlayListener, GBSIPAgentAudioBroadcastListener,
        GBSIPAgentDeviceControlListener, GBSIPAgentQueryCommandListener, 
        GBSIPAgentTalkListener, 
        GBSIPAgentQueryRecordInfoListener{
}

媒体数据处理接口,可参照SmartPublisherJniV2.java,如需语音广播或语音对讲,可参照SmartPlayerJniV2.java。

信令处理

GBSIPAgentListener主要系GB28181注册、心跳、DevicePosition等,如注册成功、注册超时、注册网络传输层错误、心跳异常、设备位置请求处理:

public interface GBSIPAgentListener
{
    /*注册成功
    * @param dateString: 服务器日期,用来校准设备端时间,用户自行决定是否校准设备时间
    */
    void ntsRegisterOK(String dateString);

    /*
    *注册超时
    */
    void ntsRegisterTimeout();

    /*
    *注册网络传输层异常
    */
    void ntsRegisterTransportError(String errorInfo);

    /*
    *心跳达到异常次数
    */
    void ntsOnHeartBeatException(int exceptionCount, String lastExceptionInfo);

    /*
     * 设备位置请求, 这个主要用在移动设备位置订阅上
     * @param interval 请求间隔, 单位是毫秒
     */
    void ntsOnDevicePositionRequest(String deviceId, int interval);
}

GBSIPAgentPlayListener主要系GB28181的Invite、Ack、Bye等处理:

public interface GBSIPAgentPlayListener {

    /*
     *收到s=Play的实时视音频点播
     */
    void ntsOnInvitePlay(String deviceId, SessionDescription sessionDescription);

    /*
     *发送play invite response 异常
     */
    void ntsOnPlayInviteResponseException(String deviceId, int statusCode, String errorInfo);

    /*
     * 收到CANCEL play INVITE请求
     */
    void ntsOnCancelPlay(String deviceId);

    /*
     * 收到Ack
     */
    void ntsOnAckPlay(String deviceId);

    /*
     * 收到Bye
     */
    void ntsOnByePlay(String deviceId);

    /*
     * 不是在收到BYE Message情况下, 终止Play
     */
    void ntsOnTerminatePlay(String deviceId);

    /*
     * Play会话对应的对话终止, 一般不会出发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发
    收到这个, 请做相关清理处理
    */
    void ntsOnPlayDialogTerminated(String deviceId);
}

GBSIPAgentAudioBroadcastListener主要系GB28181语音广播处理相关,如有语音广播相关需求,可参照demo实例实现:

public interface GBSIPAgentAudioBroadcastListener {

    /*
     *收到语音广播通知
     */
    void ntsOnNotifyBroadcastCommand(String fromUserName, String fromUserNameAtDomain, String sn, String sourceID, String targetID);

    /*
     *需要准备接受语音广播的SDP内容
     */
    void ntsOnAudioBroadcast(String commandFromUserName, String commandFromUserNameAtDomain, String sourceID, String targetID);

    /*
     *音频广播, 发送Invite请求异常
     */
    void ntsOnInviteAudioBroadcastException(String sourceID, String targetID, String errorInfo);

    /*
     *音频广播, 等待Invite响应超时
     */
    void ntsOnInviteAudioBroadcastTimeout(String sourceID, String targetID);

    /*
     *音频广播, 收到Invite消息最终响应
     */
    void ntsOnInviteAudioBroadcastResponse(String sourceID, String targetID, int statusCode, SessionDescription sessionDescription);

    /*
     * 音频广播, 收到BYE Message
     */
    void ntsOnByeAudioBroadcast(String sourceID, String targetID);


    /*
     * 不是在收到BYE Message情况下, 终止音频广播
     */
    void ntsOnTerminateAudioBroadcast(String sourceID, String targetID);
}

GBSIPAgentDeviceControlListener主要系GB28181设备控制相关,比如远程启动、云台控制:

public interface GBSIPAgentDeviceControlListener {

    /*
     * 收到远程启动控制命令
     */
    void ntsOnDeviceControlTeleBootCommand(String deviceId, String teleBootValue);

    /*
    * 云台控制
     */
    void ntsOnDeviceControlPTZCmd(String deviceId, String typeValue);
}

GBSIPAgentQueryCommandListener主要系GB28181查询命令,如预置位查询:

public interface GBSIPAgentQueryCommandListener {

    /*
     * 设备预置位查询
     */
    void ntsOnDevicePresetQueryCommand(String fromUserName, String fromUserNameAtDomain, String sn, String deviceId);
}

GBSIPAgentTalkListener主要系GB28181语音对讲相关处理:

public interface GBSIPAgentTalkListener {
    /*
     *收到s=Talk 语音对讲
     */
    void ntsOnInviteTalk(String deviceId, SessionDescription sessionDescription);

    /*
     *发送talk invite response 异常
     */
    void ntsOnTalkInviteResponseException(String deviceId, int statusCode, String errorInfo);

    /*
     * 收到CANCEL Talk INVITE请求
     */
    void ntsOnCancelTalk(String deviceId);

    /*
     * 收到Ack
     */
    void ntsOnAckTalk(String deviceId);

    /*
     * 收到Bye
     */
    void ntsOnByeTalk(String deviceId);

    /*
     * 不是在收到BYE Message情况下, 终止Talk
     */
    void ntsOnTerminateTalk(String deviceId);

    /*
     * Talk会话对应的对话终止, 一般不会出发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发
    收到这个, 请做相关清理处理
    */
    void ntsOnTalkDialogTerminated(String deviceId);
}

GBSIPAgentPlaybackListener系历史视音频回放相关:

public interface GBSIPAgentPlaybackListener {
    void ntsOnInvitePlayback(long var1, String var3, SessionDescription var4);

    void ntsOnPlaybackInviteResponseException(long var1, String var3, int var4, String var5);

    void ntsOnCancelPlayback(long var1, String var3);

    void ntsOnAckPlayback(long var1, String var3);

    void ntsOnPlaybackMANSRTSPPlayCommand(long var1, String var3);

    void ntsOnPlaybackMANSRTSPPauseCommand(long var1, String var3);

    void ntsOnPlaybackMANSRTSPScaleCommand(long var1, String var3, double var4);

    void ntsOnPlaybackMANSRTSPSeekCommand(long var1, String var3, double var4);

    void ntsOnPlaybackMANSRTSPTeardownCommand(long var1, String var3);

    void ntsOnByePlayback(long var1, String var3);

    void ntsOnTerminatePlayback(long var1, String var3);

    void ntsOnPlaybackDialogTerminated(long var1, String var3);
}

GBSIPAgentDownloadListen系历史视音频下载相关:

public interface GBSIPAgentDownloadListener {
    void ntsOnInviteDownload(long var1, String var3, SessionDescription var4);

    void ntsOnDownloadInviteResponseException(long var1, String var3, int var4, String var5);

    void ntsOnCancelDownload(long var1, String var3);

    void ntsOnAckDownload(long var1, String var3);

    void ntsOnDownloadMANSRTSPScaleCommand(long var1, String var3, double var4);

    void ntsOnByeDownload(long var1, String var3);

    void ntsOnTerminateDownload(long var1, String var3);

    void ntsOnDownloadDialogTerminated(long var1, String var3);
}

媒体数据处理

RTP数据发送

RTP Sender(SmartPublisherJniV2.java)相关接口设计:

/*
 * SmartPublisherJniV2.java
 * Author: https://daniusdk.com
 */
/*
 * 创建RTP Sender实例
 *
 * @param reserve:保留参数传0
 *
 * @return RTP Sender 句柄,0表示失败
 */
public native long CreateRTPSender(int reserve);

/**
 *设置 RTP Sender传输协议
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param transport_protocol, 0:UDP, 1:TCP, 默认是UDP
 *
 * @return {0} if successful
 */
public native int SetRTPSenderTransportProtocol(long rtp_sender_handle, int transport_protocol);

/**
 *设置 RTP Sender IP地址类型
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param ip_address_type, 0:IPV4, 1:IPV6, 默认是IPV4, 当前仅支持IPV4
 *
 * @return {0} if successful
 */
public native int SetRTPSenderIPAddressType(long rtp_sender_handle, int ip_address_type);

/**
 *设置 RTP Sender RTP Socket本地端口
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param port, 必须是偶数,设置0的话SDK会自动分配, 默认值是0
 *
 * @return {0} if successful
 */
public native int SetRTPSenderLocalPort(long rtp_sender_handle, int port);

/**
 *设置 RTP Sender SSRC
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param ssrc, 如果设置的话,这个字符串要能转换成uint32类型, 否则设置失败
 *
 * @return {0} if successful
 */
public native int SetRTPSenderSSRC(long rtp_sender_handle, String ssrc);

/**
 *设置 RTP Sender RTP socket 发送Buffer大小
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param buffer_size, 必须大于0, 默认是512*1024, 当前仅对UDP socket有效, 根据视频码率考虑设置合适的值
 *
 * @return {0} if successful
 */
public native int SetRTPSenderSocketSendBuffer(long rtp_sender_handle, int buffer_size);

/**
 *设置 RTP Sender RTP时间戳时钟频率
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param clock_rate, 必须大于0, 对于GB28181 PS规定是90kHz, 也就是90000
 *
 * @return {0} if successful
 */
public native int SetRTPSenderClockRate(long rtp_sender_handle, int clock_rate);

/**
 *设置 RTP Sender 目的IP地址, 注意当前用在GB2818推送上,只设置一个地址,将来扩展如果用在其他地方,可能要设置多个目的地址,到时候接口可能会调整
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param address, IP地址
 * @param port, 端口
 *
 * @return {0} if successful
 */
public native int SetRTPSenderDestination(long rtp_sender_handle, String address, int port);

/**
 * 设置是否开启 RTP Receiver
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param is_enable, 0表示不收RTP包, 1表示收RTP包, SDK默认值为0.
 * @return
 */
public native int EnableRTPSenderReceive(long rtp_sender_handle, int is_enable);

/**
 *设置RTP Receiver SSRC
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param ssrc, 如果设置的话,这个字符串要能转换成uint32类型, 否则设置失败
 *
 * @return {0} if successful
 */
public native int SetRTPSenderReceiveSSRC(long rtp_sender_handle, String ssrc);

/**
 *设置RTP Receiver Payload 相关信息
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 *
 * @param payload_type, 请参考 RFC 3551
 *
 * @param encoding_name, 编码名, 请参考 RFC 3551, 如果payload_type不是动态的, 可能传null就好
 *
 * @param media_type, 媒体类型, 请参考 RFC 3551, 1 是视频, 2是音频
 *
 * @param clock_rate, 请参考 RFC 3551
 *
 * @return {0} if successful
 */
public native int SetRTPSenderReceivePayloadType(long rtp_sender_handle, int payload_type, String encoding_name, int media_type, int clock_rate);

/**
 *设置RTP Receiver PS的pts和dts clock frequency
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 *
 * @param ps_clock_frequency, 默认是90000, 一些特殊场景需要设置
 *
 * @return {0} if successful
 */
public native int SetRTPSenderReceivePSClockFrequency(long rtp_sender_handle, int ps_clock_frequency);

/**
 *设置 RTP Receiver 音频采样率
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param sampling_rate, 音频采样率
 *
 * @return {0} if successful
 */
public native int SetRTPSenderReceiveAudioSamplingRate(long rtp_sender_handle, int sampling_rate);

/**
 *设置 RTP Receiver 音频通道数
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param channels, 音频通道数
 *
 * @return {0} if successful
 */
public native int SetRTPSenderReceiveAudioChannels(long rtp_sender_handle, int channels);

/**
 *初始化RTP Sender, 初始化之前先调用上面的接口配置相关参数
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 *
 * @return {0} if successful
 */
public native int InitRTPSender(long rtp_sender_handle);

/**
 *获取RTP Sender RTP Socket本地端口
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 *
 * @return 失败返回0, 成功的话返回响应的端口, 请在InitRTPSender返回成功之后调用
 */
public native int GetRTPSenderLocalPort(long rtp_sender_handle);

/**
 * UnInit RTP Sender
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 *
 * @return {0} if successful
 */
public native int UnInitRTPSender(long rtp_sender_handle);

/**
 * 释放RTP Sender, 释放之后rtp_sender_handle就无效了,请不要再使用
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 *
 * @return {0} if successful
 */
public native int DestoryRTPSender(long rtp_sender_handle);

RTP数据接收

对应RTP Receiver(SmartPlayerJniV2.java)相关接口设计,如无语音广播或语音对讲相关技术需求,这部分可忽略:

/*
 * SmartPlayerJniV2.java
 * Author: https://daniusdk.com
 */
/*
 * 创建RTP Receiver
 *
 * @param reserve:保留参数传0
 *
 * @return RTP Receiver 句柄,0表示失败
 */
public native long CreateRTPReceiver(int reserve);


/**
 *设置 RTP Receiver传输协议
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 * @param transport_protocol, 0:UDP, 1:TCP, 默认是UDP
 *
 * @return {0} if successful
 */
public native int SetRTPReceiverTransportProtocol(long rtp_receiver_handle, int transport_protocol);


/**
 *设置 RTP Receiver IP地址类型
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 * @param ip_address_type, 0:IPV4, 1:IPV6, 默认是IPV4
 *
 * @return {0} if successful
 */
public native int SetRTPReceiverIPAddressType(long rtp_receiver_handle, int ip_address_type);


/**
 *设置 RTP Receiver RTP Socket本地端口
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 * @param port, 必须是偶数,设置0的话SDK会自动分配, 默认值是0
 *
 * @return {0} if successful
 */
public native int SetRTPReceiverLocalPort(long rtp_receiver_handle, int port);


/**
 *设置 RTP Receiver SSRC
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 * @param ssrc, 如果设置的话,这个字符串要能转换成uint32类型, 否则设置失败
 *
 * @return {0} if successful
 */
public native int SetRTPReceiverSSRC(long rtp_receiver_handle, String ssrc);


/**
 *创建 RTP Receiver 会话
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 * @param reserve, 保留值,目前传0
 *
 * @return {0} if successful
 */
public native int CreateRTPReceiverSession(long rtp_receiver_handle, int reserve);


/**
 *获取 RTP Receiver RTP Socket本地端口
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 *
 * @return 失败返回0, 成功的话返回响应的端口, 请在CreateRTPReceiverSession返回成功之后调用
 */
public native int GetRTPReceiverLocalPort(long rtp_receiver_handle);


/**
 *设置 RTP Receiver Payload 相关信息
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 *
 * @param payload_type, 请参考 RFC 3551
 *
 * @param encoding_name, 编码名, 请参考 RFC 3551, 如果payload_type不是动态的, 可能传null就好
 *
 * @param media_type, 媒体类型, 请参考 RFC 3551, 1 是视频, 2是音频
 *
 * @param clock_rate, 请参考 RFC 3551
 *
 * @return {0} if successful
 */
public native int SetRTPReceiverPayloadType(long rtp_receiver_handle, int payload_type, String encoding_name, int media_type, int clock_rate);


/**
 *设置 RTP Receiver 音频采样率
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 * @param sampling_rate, 音频采样率
 *
 * @return {0} if successful
 */
public native int SetRTPReceiverAudioSamplingRate(long rtp_receiver_handle, int sampling_rate);

/**
 *设置 RTP Receiver 音频通道数
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 * @param channels, 音频通道数
 *
 * @return {0} if successful
 */
public native int SetRTPReceiverAudioChannels(long rtp_receiver_handle, int channels);


/**
 *设置 RTP Receiver 远端地址
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 * @param address, IP地址
 * @param port, 端口
 *
 * @return {0} if successful
 */
public native int SetRTPReceiverRemoteAddress(long rtp_receiver_handle, String address, int port);

/**
 *初始化 RTP Receiver
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 *
 * @return {0} if successful
 */
public native int InitRTPReceiver(long rtp_receiver_handle);

/**
 *UnInit RTP Receiver
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 *
 * @return {0} if successful
 */
public native int UnInitRTPReceiver(long rtp_receiver_handle);


/**
 *Destory RTP Receiver Session
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 *
 * @return {0} if successful
 */
public native int DestoryRTPReceiverSession(long rtp_receiver_handle);


/**
 *Destory RTP Receiver
 *
 * @param rtp_receiver_handle, CreateRTPReceiver
 *
 * @return {0} if successful
 */
public native int DestoryRTPReceiver(long rtp_receiver_handle);

PostAudioPacket(SmartPlayerJniV2.java),投递音频包给外部Live source,目前仅于语音对讲使用:

/*
 * SmartPlayerJniV2.java
 * Author: https://daniusdk.com
 */
/**
 * 投递音频包给外部Live source, 注意ByteBuffer对象必须是DirectBuffer
 *
 * @param handle: return value from SmartPlayerOpen()
 *
 * @return {0} if successful
 */
public native int PostAudioPacket(long handle, int codec_id,
                          java.nio.ByteBuffer packet, int offset, int size, long pts, boolean is_pts_discontinuity,
                          java.nio.ByteBuffer extra_data, int extra_data_offset, int extra_data_size, int sample_rate, int channels);

GB28181接口调用

对应GB28181相关接口调用相关设计如下:

/*
 * SmartPublisherJniV2.java
 * Author: https://daniusdk.com
 */
/**
 * 设置GB28181 RTP Sender
 *
 * @param rtp_sender_handle, CreateRTPSender返回值
 * @param rtp_payload_type, 对于GB28181 PS, 协议定义是96, 具体以SDP为准,  RFC 3551有定义
 * @param encoding_name, 编码名, 请参考 RFC 3551, 当前仅支持: "PS", 其他值返回失败
 * @return {0} if successful
 */
public native int SetGB28181RTPSender(long handle, long rtp_sender_handle, int rtp_payload_type, String encoding_name);

/**
 * 设置GB28181 RTP 收到的音频包回调
 * @param handle
 * @param audio_packet_callback
 * @return
 */
public native int SetGB28181ReceiveAudioPacketCallback(long handle, NTAudioPacketCallback audio_packet_callback);

/**
 * 启动 GB28181 媒体流
 *
 * @return {0} if successful
 */
public native int StartGB28181MediaStream(long handle);

/**
 * 停止 GB28181 媒体流
 *
 * @return {0} if successful
 */
public native int StopGB28181MediaStream(long handle);

总结

以上是大牛直播SDK发布的Android平台GB28181设备接入模块的相关说明,除了上述接口设计外,模块还可以扩展实现实时静音、实时快照、按需录像、实时音量调节等,可扩展性非常好。

基于arm64架构国产操作系统|Linux下的RTMP|RTSP低延时直播播放器开发探究

技术背景

2014年4月8日起,美国微软公司停止了对Windows XP SP3操作系统提供服务支持,这引起了社会和广大用户的广泛关注和对信息安全的担忧。而2020年对Windows7服务支持的终止再一次推动了国产系统的发展。工信部对此表示,将继续加大力度,支持Linux的国产操作系统的研发和应用,并希望用户可以使用国产操作系统。

为什么要发展国产操作系统?

  1. 技术自主性和信息安全:国家和政府机构通常认为拥有自主研发和掌握核心技术是保障国家信息安全和加强自主创新能力的重要举措。通过开发和商业化国产操作系统,可以降低对外国技术和软件的依赖,并减少潜在的信息泄露和安全风险。
  2. 降低技术依赖风险:依赖外国操作系统和软件可能面临技术封锁、供应中断或不稳定的问题。商业化国产操作系统可以减少对外部技术供应链的依赖,为国内企业和用户提供更可靠和稳定的软件解决方案,降低技术依赖风险。
  3. 满足国内市场需求:随着国内科技产业的快速发展,对于操作系统的需求也日益增长。国产操作系统可以更好地满足国内用户的需求,提供更贴近本地文化和习惯的用户界面和功能,提高用户的使用体验。
  4. 促进产业发展:国产操作系统的开发和应用可以带动相关产业的发展,包括硬件制造、软件开发、系统集成等。这有助于提升整个产业链的技术水平和竞争力,促进国内科技产业的升级和转型。
  5. 生态系统建设:发展国产操作系统有助于构建和完善自主可控的软件生态系统。通过鼓励和支持国内软件开发者在国产操作系统上进行应用开发,可以丰富应用生态,提高系统的可用性和易用性。

此外,随着云计算、大数据、人工智能等新兴技术的发展,操作系统作为基础设施的重要性日益凸显。发展国产操作系统可以为这些新兴技术提供更安全、更可靠的运行环境,推动相关产业的发展和创新。

综上所述,发展国产操作系统对于保障国家信息安全、降低技术依赖风险、满足国内市场需求、促进产业发展以及构建完善的生态系统等方面都具有重要意义。

技术实现

顺势而为,在发布arm64架构的国产操作系统|Linux平台的RTMP|RTSP直播播放SDK之前,大牛直播SDK(官方)的直播播放SDK用一句比较流行的广告语叫遥遥领先,我们更是在前几年已经发布了Linux X86_64架构的播放器,并得到了广泛的应用。

本次发布的可用于国产操作系统和Linux上的的RTMP|RTSP直播播放SDK, video输出基于X协议,audio输出采用PulseAudio和Alsa Lib实现。除了常规功能如实时静音、快照、buffer time设定、网络自动重连等,RTMP支持扩展H265播放(支持Enhanced RTMP H.265播放), RTSP也支持H265播放。

大牛直播SDK发布的Linux平台播放器SDK支持多实例播放,相关代码如下:

/*
 * multi_player_demo.cpp
 * 
 * Author: daniusdk.com
 *
 * Copyright © 2017~2024 DaniuLive. All rights reserved.
 */
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <poll.h>
#include <errno.h>

#include <string>
#include <sstream>

#include <X11/Xlib.h>
#include <X11/keysym.h>

#include "nt_sdk_linux_smart_log.h"
#include "nt_linux_smart_player_sdk.h"
#include "nt_player_sdk_wrapper.h"

....

const char* players_url_[]
{
	"rtsp://admin:daniulive12345@192.168.0.120:554/h264/ch1/main/av_stream",
	"rtsp://admin:daniulive12345@192.168.0.120:554/h264/ch1/main/av_stream",
	"rtsp://admin:admin123456@192.168.0.121:554/cam/realmonitor?channel=1&subtype=0",
	"rtsp://admin:admin123456@192.168.0.121:554/cam/realmonitor?channel=1&subtype=0",
};

int main(int argc, char *argv[])
{
	XInitThreads(); // X支持多线程, 必须调用

	NT_SDKLogInit();

	// SDK初始化
	SmartPlayerSDKAPI player_api;
	if (!NT_PlayerSDKInit(player_api))
	{
		fprintf(stderr, "SDK init failed.\n");
		return 0;
	}

	auto display = XOpenDisplay(nullptr);
	if (!display)
	{
		fprintf(stderr, "Cannot connect to X server\n");
		player_api.UnInit();
		return 0;
	}

	auto screen = DefaultScreen(display);
	auto root = XRootWindow(display, screen);

	XWindowAttributes root_win_att;
	if (!XGetWindowAttributes(display, root, &root_win_att))
	{
		fprintf(stderr, "Get Root window attri failed\n");
		player_api.UnInit();
		XCloseDisplay(display);
		return 0;
	}

	if (root_win_att.width < 100 || root_win_att.height < 100)
	{
		fprintf(stderr, "Root window size error.\n");
		player_api.UnInit();
		XCloseDisplay(display);
		return 0;
	}

	fprintf(stdout, "Root Window Size:%d*%d\n", root_win_att.width, root_win_att.height);

	int main_w = root_win_att.width / 2, main_h = root_win_att.height/2;

	auto black_pixel = BlackPixel(display, screen);
	auto white_pixel = WhitePixel(display, screen);

	auto main_wid = XCreateSimpleWindow(display, root, 0, 0, main_w, main_h, 0, white_pixel, black_pixel);
	if (!main_wid)
	{
		player_api.UnInit();
		XCloseDisplay(display);
		fprintf(stderr, "Cannot create main windows\n");
		return 0;
	}

	XSelectInput(display, main_wid, StructureNotifyMask | KeyPressMask);

	XMapWindow(display, main_wid);
	XStoreName(display, main_wid, win_base_title);

	std::vector<std::shared_ptr<NT_PlayerSDKWrapper> > players;

	for (auto url: players_url_)
	{
		auto i = std::make_shared<NT_PlayerSDKWrapper>(&player_api);
		i->SetDisplay(display);
		i->SetScreen(screen);
		i->SetURL(url);
		players.push_back(i);

		if ( players.size() > 3 )
			break;
	}

	auto border_w = 2;

	std::vector<NT_LayoutRect> layout_rects;
	SubWindowsLayout(main_w, main_h, border_w, static_cast<int>(players.size()), layout_rects);

	for (auto i = 0; i < static_cast<int>(players.size()); ++i)
	{
		assert(players[i]);
		players[i]->SetWindow(CreateSubWindow(display, screen, main_wid, layout_rects[i], border_w));
	}

	for (const auto& i : players)
	{
		assert(i);
		if (i->GetWindow())
			XMapWindow(display, i->GetWindow());
	}

	for (auto i = 0; i < static_cast<int>(players.size()); ++i)
	{
		assert(players[i]);
		// 第一路不静音, 其他全部静音
		players[i]->Start(0, i!=0, 1, false);
		//players[i]->Start(0, false, 1, false);
	}

	while (true)
	{
		while (MY_X11_Pending(display, 10))
		{
			XEvent xev;
			memset(&xev, 0, sizeof(xev));
			XNextEvent(display, &xev);

			if (xev.type == ConfigureNotify)
			{
				if (xev.xconfigure.window == main_wid)
				{
					if (xev.xconfigure.width != main_w || xev.xconfigure.height != main_h)
					{
						main_w = xev.xconfigure.width;
						main_h = xev.xconfigure.height;

						SubWindowsLayout(main_w, main_h, border_w, static_cast<int>(players.size()), layout_rects);

						for (auto i = 0; i < static_cast<int>(players.size()); ++i)
						{
							if (players[i]->GetWindow())
							{
								XMoveResizeWindow(display, players[i]->GetWindow(), layout_rects[i].x_, layout_rects[i].y_, layout_rects[i].w_, layout_rects[i].h_);
							}
						}
					}
				}
				else
				{
					for (const auto& i: players)
					{
						assert(i);
						if (i->GetWindow() && i->GetWindow() == xev.xconfigure.window)
						{
							i->OnWindowSize(xev.xconfigure.width, xev.xconfigure.height);
						}
					}
				}
			}
			else if (xev.type == KeyPress)
			{
				if (xev.xkey.keycode == XKeysymToKeycode(display, XK_Escape))
				{
					fprintf(stdout, "ESC Key Press\n");

					for (const auto& i : players)
					{
						i->Stop();

						if (i->GetWindow())
						{
							XDestroyWindow(display, i->GetWindow());
							i->SetWindow(None);
						}
					}

					players.clear();
					
					XDestroyWindow(display, main_wid);
					XCloseDisplay(display);

					player_api.UnInit();

					fprintf(stdout, "Close Players....\n");
					return 0;
				}
			}
		}
	}
}

开始播放、停止播放封装实现

bool NT_PlayerSDKWrapper::Start(int buffer, bool is_mute, int render_scale_mode, bool is_only_dec_key_frame)
{
	if (is_playing_)
		return false;

	if (url_.empty())
		return false;

	if (!OpenHandle(url_, buffer))
		return false;

	assert(handle_ && handle_->Handle());

	// 音频参数
	player_api_->SetMute(handle_->Handle(), is_mute ? 1 : 0);
	player_api_->SetIsOutputAudioDevice(handle_->Handle(), 1);
	player_api_->SetAudioOutputLayer(handle_->Handle(), 0); // 使用pluse 或者 alsa播放, 两个可以选择一个

	// 视频参数
	player_api_->SetVideoSizeCallBack(handle_->Handle(), this, &NT_Player_SDK_WRAPPER_OnVideoSizeHandle);
	//player_api_->SetXDisplayName(handle_->Handle(), NULL);
	player_api_->SetXScreenNumber(handle_->Handle(),screen_);
	player_api_->SetRenderXWindow(handle_->Handle(), window_);
	player_api_->SetRenderScaleMode(handle_->Handle(), render_scale_mode);
	player_api_->SetRenderTextureScaleFilterMode(handle_->Handle(), 3);

	player_api_->SetOnlyDecodeVideoKeyFrame(handle_->Handle(), is_only_dec_key_frame ? 1 : 0);

	auto ret = player_api_->StartPlay(handle_->Handle());
	if (NT_ERC_OK != ret)
	{
		ResetHandle();
		return false;
	}

	is_playing_ = true;

	return true;
}

void NT_PlayerSDKWrapper::Stop()
{
	if (!is_playing_)
		return;

	assert(handle_);
	player_api_->StopPlay(handle_->Handle());

	video_width_ = 0;
	video_height_ = 0;

	ResetHandle();

	is_playing_ = false;
}

Event回调

bool NT_PlayerSDKWrapper::AttachHandle(const std::shared_ptr<NT_SDK_HandleWrapper>& handle)
{
	if (is_playing_)
		return false;

	handle_ = handle;

	if (handle_)
	{
		handle_->AddEventHandler(shared_from_this());
	}

	return true;
}

视频分辨率回调

extern "C" NT_VOID NT_CALLBACK NT_Player_SDK_WRAPPER_OnVideoSizeHandle(NT_HANDLE handle, NT_PVOID user_data,
	NT_INT32 width, NT_INT32 height)
{
	auto sdk_wrapper = reinterpret_cast<NT_PlayerSDKWrapper*>(user_data);
	if (nullptr == sdk_wrapper)
		return;

	sdk_wrapper->VideoSizeHandle(handle, width, height);
}

实时快照回调

extern "C" NT_VOID NT_CALLBACK NT_Player_SDK_WRAPPER_OnCaptureImageCallBack(NT_HANDLE handle, NT_PVOID user_data, NT_UINT32 result, NT_PCSTR file_name)
{
	auto sdk_wrapper = reinterpret_cast<NT_PlayerSDKWrapper*>(user_data);
	if (nullptr == sdk_wrapper)
		return;

	sdk_wrapper->CaptureImageHandle(handle, result, file_name);
}

总结

arm64架构的国产操作系统|Linux下的RTMP、RTSP直播播放,延迟依然毫秒级,随着国产操作系统在传统行业的推进,越来越多的场景需要高稳定性高延迟低的RTMP|RTSP播放器,本文抛砖引玉,感兴趣的开发者可以跟我单独探讨。

Android平台轻量级RTSP服务模块技术接入说明

技术背景

为满足内网无纸化/电子教室等内网超低延迟需求,避免让用户配置单独的服务器,大牛直播SDK在推送端发布了轻量级RTSP服务SDK。

轻量级RTSP服务解决的核心痛点是避免用户或者开发者单独部署RTSP或者RTMP服务,实现本地的音视频数据(如摄像头、麦克风),编码后,汇聚到内置RTSP服务,对外提供可供拉流的RTSP URL,轻量级RTSP服务,适用于内网环境下,对并发要求不高的场景,支持H.264/H.265,支持RTSP鉴权、单播、组播模式,考虑到单个服务承载能力,我们支持同时创建多个RTSP服务,并支持获取当前RTSP服务会话连接数。

轻量级RTSP服务数据源,支持编码前、编码后数据对接:

  • 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型);
  • 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);
  • 拉取RTSP或RTMP流并注入轻量级RTSP服务模块,组合形成内置RTSP网关模块。

技术对接

 系统要求

  • SDK支持Android5.1及以上版本;
  • 支持的CPU架构:armv7, arm64, x86, x86_64。

准备工作

  • 确保SmartPublisherJniV2.java放到com.daniulive.smartpublisher包名下(可在其他包名下调用);
  • smartavengine.jar加入到工程;
  • 拷贝libSmartPublisher.so到工程;
  • AndroidManifast.xml添加相关权限:
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />

  • Load相关so:
static {  
    System.loadLibrary("SmartPublisher");
}

  • build.gradle配置32/64位库:
splits {
    abi {
        enable true
        reset()
        // Specifies a list of ABIs that Gradle should create APKs for
        include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' //select ABIs to build APKs for
        // Specify that we do not want to also generate a universal APK that includes all ABIs
        universalApk true
    }
}

  • 如需集成到自己系统测试,请用大牛直播SDK的app name,授权版按照授权app name正常使用即可;
  • 如何改app-name,strings.xml做以下修改:
<string name="app_name">SmartPublisherSDKDemo</string>

接口设计

Android内置轻量级RTSP服务SDK接口详解
调用描述 接口 接口描述
SmartRTSPServerSDK
初始化RTSP Server InitRtspServer Init rtsp server(和UnInitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次InitRtspServer,请确保在OpenRtspServer之前调用)
创建一个rtsp server OpenRtspServer 创建一个rtsp server,返回rtsp server句柄
设置端口 SetRtspServerPort 设置rtsp server 监听端口, 在StartRtspServer之前必须要设置端口
设置鉴权用户名、密码 SetRtspServerUserNamePassword 设置rtsp server 鉴权用户名和密码, 这个可以不设置,只有需要鉴权的再设置
获取rtsp server当前会话数 GetRtspServerClientSessionNumbers 获取rtsp server当前的客户会话数, 这个接口必须在StartRtspServer之后再调用
启动rtsp server StartRtspServer 启动rtsp server
停止rtsp server StopRtspServer 停止rtsp server
关闭rtsp server CloseRtspServer 关闭rtsp server
UnInit rtsp server UnInitRtspServer UnInit rtsp server(和InitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次UnInitRtspServer)
SmartRTSPServerSDK供Publisher调用的接口
设置rtsp的流名称 SetRtspStreamName 设置rtsp的流名称
给要发布的rtsp流设置rtsp server AddRtspStreamServer 给要发布的rtsp流设置rtsp server, 一个流可以发布到多个rtsp server上,rtsp server的创建启动请参考OpenRtspServer和StartRtspServer接口
清除设置的rtsp server ClearRtspStreamServer 清除设置的rtsp server
启动rtsp流 StartRtspStream 启动rtsp流
停止rtsp流 StopRtspStream 停止rtsp流

功能支持

  •  ​[视频格式]H.264/H.265(Android H.265硬编码);
  •  [音频格式]G.711 A律、AAC;
  • 协议:RTSP;
  •  [音量调节]Android平台采集端支持实时音量调节;
  •  [H.264硬编码]支持H.264特定机型硬编码;
  •  [H.265硬编码]支持H.265特定机型硬编码;
  • [音视频]支持纯音频/纯视频/音视频;
  • [摄像头]支持采集过程中,前后摄像头实时切换;
  • 支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置;
  • [实时水印]支持动态文字水印、png水印;
  • [实时快照]支持实时快照;
  • [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
  • [外部编码前视频数据对接]支持YUV数据对接;
  • [外部编码前音频数据对接]支持PCM对接;
  • [外部编码后视频数据对接]支持外部H.264、H.265数据对接;
  • [外部编码后音频数据对接]外部AAC数据对接;
  • [扩展录像功能]支持和录像SDK组合使用,录像相关功能。​
  • 支持RTSP端口设置;
  • 支持RTSP鉴权用户名、密码设置;
  • 支持获取当前RTSP服务会话连接数;
  • 支持Android 5.1及以上版本。

接口调用详解

本文以大牛直播SDK Android平台Camera2Demo为例,启动RTSP服务、发布RTSP流之前,可以先选择视频分辨率、软编还是硬编码,音频是PCMA还是AAC编码等基础设置,其他参数的设置,可以参考下面InitAndSetConfig()。

以Android平台Camera2对接为例,先初始化RTSP Server:

/*
 * MainActivity.java
 * Author: daniusdk.com
 */
@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
	
	...

	context_ = this.getApplicationContext();
	
	libPublisher = new SmartPublisherJniV2();

	libPublisher.InitRtspServer(context_);      //和UnInitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次InitRtspServer,请确保在OpenRtspServer之前调用
}

启动、停止RTSP服务:

//启动/停止RTSP服务
class ButtonRtspServiceListener implements View.OnClickListener {
	public void onClick(View v) {
		if (isRTSPServiceRunning) {
			stopRtspService();

			btnRtspService.setText("启动RTSP服务");
			btnRtspPublisher.setEnabled(false);

			isRTSPServiceRunning = false;
			return;
		}

		Log.i(TAG, "onClick start rtsp service..");

		rtsp_handle_ = libPublisher.OpenRtspServer(0);

		if (rtsp_handle_ == 0) {
			Log.e(TAG, "创建rtsp server实例失败! 请检查SDK有效性");
		} else {
			int port = 8554;
			if (libPublisher.SetRtspServerPort(rtsp_handle_, port) != 0) {
				libPublisher.CloseRtspServer(rtsp_handle_);
				rtsp_handle_ = 0;
				Log.e(TAG, "创建rtsp server端口失败! 请检查端口是否重复或者端口不在范围内!");
			}

			if (libPublisher.StartRtspServer(rtsp_handle_, 0) == 0) {
				Log.i(TAG, "启动rtsp server 成功!");
			} else {
				libPublisher.CloseRtspServer(rtsp_handle_);
				rtsp_handle_ = 0;
				Log.e(TAG, "启动rtsp server失败! 请检查设置的端口是否被占用!");
			}

			btnRtspService.setText("停止RTSP服务");
			btnRtspPublisher.setEnabled(true);

			isRTSPServiceRunning = true;
		}
	}
}

stopRtspService()实现如下:

//停止RTSP服务
private void stopRtspService() {
	if(!isRTSPServiceRunning)
	{
		return;
	}
	if (libPublisher != null && rtsp_handle_ != 0) {
		libPublisher.StopRtspServer(rtsp_handle_);
		libPublisher.CloseRtspServer(rtsp_handle_);
		rtsp_handle_ = 0;
	}
}

发布、停止RTSP流:

//发布/停止RTSP流
class ButtonRtspPublisherListener implements View.OnClickListener {
	public void onClick(View v) {
		if (stream_publisher_.is_rtsp_publishing()) {
			stopRtspPublisher();

			btnRtspPublisher.setText("发布RTSP流");
			btnGetRtspSessionNumbers.setEnabled(false);
			btnRtspService.setEnabled(true);
			return;
		}

		Log.i(TAG, "onClick start rtsp publisher..");

		InitAndSetConfig();

		String rtsp_stream_name = "stream1";
		stream_publisher_.SetRtspStreamName(rtsp_stream_name);
		stream_publisher_.ClearRtspStreamServer();

		stream_publisher_.AddRtspStreamServer(rtsp_handle_);

		if (!stream_publisher_.StartRtspStream()) {
			stream_publisher_.try_release();
			Log.e(TAG, "调用发布rtsp流接口失败!");
			return;
		}

		startAudioRecorder();
		startLayerPostThread();

		btnRtspPublisher.setText("停止RTSP流");
		btnGetRtspSessionNumbers.setEnabled(true);
		btnRtspService.setEnabled(false);
	}
}

stopRtspPublisher()实现如下:

//停止发布RTSP流
private void stopRtspPublisher() {
	stream_publisher_.StopRtspStream();
	stream_publisher_.try_release();

	if (!stream_publisher_.is_publishing())
		stopAudioRecorder();
}

其中,InitAndSetConfig()实现如下,通过调研SmartPublisherOpen()接口,生成推送实例句柄。

/*
 * MainActivity.java
 * Author: daniusdk.com
 */
private void InitAndSetConfig() {
	if (null == libPublisher)
		return;

	if (!stream_publisher_.empty())
		return;

	Log.i(TAG, "InitAndSetConfig video width: " + video_width_ + ", height" + video_height_ + " imageRotationDegree:" + cameraImageRotationDegree_);

	int audio_opt = 1;
	long handle = libPublisher.SmartPublisherOpen(context_, audio_opt, 3,  video_width_, video_height_);
	if (0==handle) {
		Log.e(TAG, "sdk open failed!");
		return;
	}

	Log.i(TAG, "publisherHandle=" + handle);

	int fps = 25;
	int gop = fps * 3;

	initialize_publisher(libPublisher, handle, video_width_, video_height_, fps, gop);

	stream_publisher_.set(libPublisher, handle);
}

对应的initialize_publisher()实现如下,设置软硬编码、帧率、关键帧间隔等。

private boolean initialize_publisher(SmartPublisherJniV2 lib_publisher, long handle, int width, int height, int fps, int gop) {
	if (null == lib_publisher) {
		Log.e(TAG, "initialize_publisher lib_publisher is null");
		return false;
	}

	if (0 == handle) {
		Log.e(TAG, "initialize_publisher handle is 0");
		return false;
	}

	if (videoEncodeType == 1) {
		int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, true);
		Log.i(TAG, "h264HWKbps: " + kbps);
		int isSupportH264HWEncoder = lib_publisher.SetSmartPublisherVideoHWEncoder(handle, kbps);
		if (isSupportH264HWEncoder == 0) {
			lib_publisher.SetNativeMediaNDK(handle, 0);
			lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
			lib_publisher.SetVideoHWEncoderQuality(handle, 39);
			lib_publisher.SetAVCHWEncoderProfile(handle, 0x08); // 0x01: Baseline, 0x02: Main, 0x08: High

			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x200); // Level 3.1
			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x400); // Level 3.2
			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x800); // Level 4
			lib_publisher.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1 多数情况下,这个够用了
			//lib_publisher.SetAVCHWEncoderLevel(handle, 0x2000); // Level 4.2

			// lib_publisher.SetVideoHWEncoderMaxBitrate(handle, ((long)h264HWKbps)*1300);

			Log.i(TAG, "Great, it supports h.264 hardware encoder!");
		}
	} else if (videoEncodeType == 2) {
		int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, false);
		Log.i(TAG, "hevcHWKbps: " + kbps);
		int isSupportHevcHWEncoder = lib_publisher.SetSmartPublisherVideoHevcHWEncoder(handle, kbps);
		if (isSupportHevcHWEncoder == 0) {
			lib_publisher.SetNativeMediaNDK(handle, 0);
			lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
			lib_publisher.SetVideoHWEncoderQuality(handle, 39);

			// libPublisher.SetVideoHWEncoderMaxBitrate(handle, ((long)hevcHWKbps)*1200);

			Log.i(TAG, "Great, it supports hevc hardware encoder!");
		}
	}

	boolean is_sw_vbr_mode = true;
	//H.264 software encoder
	if (is_sw_vbr_mode) {
		int is_enable_vbr = 1;
		int video_quality = LibPublisherWrapper.estimate_video_software_quality(width, height, true);
		int vbr_max_kbps = LibPublisherWrapper.estimate_video_vbr_max_kbps(width, height, fps);
		lib_publisher.SmartPublisherSetSwVBRMode(handle, is_enable_vbr, video_quality, vbr_max_kbps);
	}

	if (is_pcma_) {
		lib_publisher.SmartPublisherSetAudioCodecType(handle, 3);
	} else {
		lib_publisher.SmartPublisherSetAudioCodecType(handle, 1);
	}

	lib_publisher.SetSmartPublisherEventCallbackV2(handle, new EventHandlerPublisherV2().set(handler_, record_executor_));

	lib_publisher.SmartPublisherSetSWVideoEncoderProfile(handle, 3);

	lib_publisher.SmartPublisherSetSWVideoEncoderSpeed(handle, 2);

	lib_publisher.SmartPublisherSetGopInterval(handle, gop);

	lib_publisher.SmartPublisherSetFPS(handle, fps);

	// lib_publisher.SmartPublisherSetSWVideoBitRate(handle, 600, 1200);

	boolean is_noise_suppression = true;
	lib_publisher.SmartPublisherSetNoiseSuppression(handle, is_noise_suppression ? 1 : 0);

	boolean is_agc = false;
	lib_publisher.SmartPublisherSetAGC(handle, is_agc ? 1 : 0);

	int echo_cancel_delay = 0;
	lib_publisher.SmartPublisherSetEchoCancellation(handle, 1, echo_cancel_delay);

	return true;
}

发布RTSP流成功后,会回调上来可供拉流的RTSP URL:

private static class EventHandlerPublisherV2 implements NTSmartEventCallbackV2 {
	@Override
	public void onNTSmartEventCallbackV2(long handle, int id, long param1, long param2, String param3, String param4, Object param5) {

		switch (id) {
			...
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL:
				publisher_event = "RTSP服务URL: " + param3;
				break;
		}
	}
}

获取RTSP Session会话数:

//获取RTSP会话数
class ButtonGetRtspSessionNumbersListener implements View.OnClickListener {
	public void onClick(View v) {
		if (libPublisher != null && rtsp_handle_ != 0) {
			int session_numbers = libPublisher.GetRtspServerClientSessionNumbers(rtsp_handle_);

			Log.i(TAG, "GetRtspSessionNumbers: " + session_numbers);

			PopRtspSessionNumberDialog(session_numbers);
		}
	}
}

//当前RTSP会话数弹出框
private void PopRtspSessionNumberDialog(int session_numbers) {
	final EditText inputUrlTxt = new EditText(this);
	inputUrlTxt.setFocusable(true);
	inputUrlTxt.setEnabled(false);

	String session_numbers_tag = "RTSP服务当前客户会话数: " + session_numbers;
	inputUrlTxt.setText(session_numbers_tag);

	AlertDialog.Builder builderUrl = new AlertDialog.Builder(this);
	builderUrl
			.setTitle("内置RTSP服务")
			.setView(inputUrlTxt).setNegativeButton("确定", null);
	builderUrl.show();
}

数据投递如下(以Camera2采集为例,如果是其他视频格式,也可以正常对接):

@Override
public void onCameraImageData(Image image) {
	....
	for (LibPublisherWrapper i : publisher_array_)
		i.PostLayerImageYUV420888ByteBuffer(0, 0, 0,
			planes[0].getBuffer(), y_offset, planes[0].getRowStride(),
			planes[1].getBuffer(), u_offset, planes[1].getRowStride(),
			planes[2].getBuffer(), v_offset, planes[2].getRowStride(), planes[1].getPixelStride(),
			w, h, 0, 0,
			scale_w, scale_h, scale_filter_mode, rotation_degree);

}

音频采集投递设计如下:

void startAudioRecorder() {
	if (audio_recorder_ != null)
		return;

	audio_recorder_ = new NTAudioRecordV2(this);

	Log.i(TAG, "startAudioRecorder call audio_recorder_.start()+++...");

	audio_recorder_callback_ = new NTAudioRecordV2CallbackImpl(stream_publisher_, null);

	audio_recorder_.AddCallback(audio_recorder_callback_);

	if (!audio_recorder_.Start(is_pcma_ ? 8000 : 44100, 1) ) {
		audio_recorder_.RemoveCallback(audio_recorder_callback_);
		audio_recorder_callback_ = null;

		audio_recorder_ = null;

		Log.e(TAG, "startAudioRecorder start failed.");
	}
	else {
		Log.i(TAG, "startAudioRecorder call audio_recorder_.start() OK---...");
	}
}

void stopAudioRecorder() {
	if (null == audio_recorder_)
		return;

	Log.i(TAG, "stopAudioRecorder+++");

	audio_recorder_.Stop();

	if (audio_recorder_callback_ != null) {
		audio_recorder_.RemoveCallback(audio_recorder_callback_);
		audio_recorder_callback_ = null;
	}

	audio_recorder_ = null;

	Log.i(TAG, "stopAudioRecorder---");
}

回调Audio数据的地方,直接投递出去:

private static class NTAudioRecordV2CallbackImpl implements NTAudioRecordV2Callback {
	private WeakReference<LibPublisherWrapper> publisher_0_;
	private WeakReference<LibPublisherWrapper> publisher_1_;

	public NTAudioRecordV2CallbackImpl(LibPublisherWrapper publisher_0) {
		if (publisher_0 != null)
			publisher_0_ = new WeakReference<>(publisher_0);
	}

	private final LibPublisherWrapper get_publisher_0() {
		if (publisher_0_ !=null)
			return publisher_0_.get();

		return null;
	}

	@Override
	public void onNTAudioRecordV2Frame(ByteBuffer data, int size, int sampleRate, int channel, int per_channel_sample_number) {

		LibPublisherWrapper publisher_0 = get_publisher_0();
		if (publisher_0 != null)
			publisher_0.OnPCMData(data, size, sampleRate, channel, per_channel_sample_number);
	}
}

onDestroy() 的时候,调研UnInitRtspServer()即可:

@Override
protected void onDestroy() {
	Log.i(TAG, "activity destory!");

	stopAudioRecorder();

	stopRtspPublisher();
	stopRtspService();
	isRTSPServiceRunning = false;

	stream_publisher_.release();

	if (libPublisher != null)
		libPublisher.UnInitRtspServer();      //如已启用内置服务功能(InitRtspServer),调用UnInitRtspServer, 注意,即便是启动多个RTSP服务,也只需调用UnInitRtspServer一次

	stopLayerPostThread();

	if (camera2Helper != null) {
		camera2Helper.release();
	}

	super.onDestroy();
}

总结

以上是Android平台轻量级RTSP服务模块详细的对接说明,除了可以对接编码前音视频数据外,模块还支持对接编码后音视频数据,并实现本地录像、快照等功能组合使用。感兴趣的开发者,可以单独跟我们探讨。

Android平台RTSP|RTMP直播播放器技术接入说明

技术背景

大牛直播SDK自2015年发布RTSP、RTMP直播播放模块,迭代从未停止,SmartPlayer功能强大、性能强劲、高稳定、超低延迟、超低资源占用。无需赘述,全自研内核,行业内一致认可的跨平台RTSP、RTMP直播播放器。本文以Android平台为例,介绍下如何集成RTSP、RTMP播放模块。

技术对接

 系统要求

  • SDK支持Android5.1及以上版本;
  • 支持的CPU架构:armv7, arm64, x86, x86_64。

准备工作

  • 确保SmartPlayerJniV2.java放到com.daniulive.smartplayer包名下(可在其他包名下调用);
  • Smartavengine.jar加入到工程;
  • 拷贝SmartPlayerV2\app\src\main\jniLibs\armeabi-v7a、 SmartPlayerV2\app\src\main\jniLibs\arm64-v8a、SmartPlayerV2\app\src\main\jniLibs\x86和SmartPlayerV2\app\src\main\jniLibs\x86_64 下 libSmartPlayer.so到工程;
  • AndroidManifast.xml添加相关权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" >
</uses-permission>
<uses-permission android:name="android.permission.INTERNET" >
</uses-permission>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

  • Load相关so:
static {  
    System.loadLibrary("SmartPlayer");
}

  • build.gradle配置32/64位库:
splits {
    abi {
        enable true
        reset()
        // Specifies a list of ABIs that Gradle should create APKs for
        include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' //select ABIs to build APKs for
        // Specify that we do not want to also generate a universal APK that includes all ABIs
        universalApk true
    }
}

  • 如需集成到自己系统测试,请用大牛直播SDK的app name,授权版按照授权app name正常使用即可;
  • 如何改app-name,strings.xml做以下修改:
<string name="app_name">SmartPlayerSDKDemo</string>

接口设计

Android RTSP|RTMP播放端SDK接口详解
调用描述 接口 接口描述
最先调用,如成功返回播放实例 SmartPlayerOpen player初始化,设置上下文信息,返回player句柄
Event回调 SetSmartPlayerEventCallbackV2 设置event callback
硬解码设置H.264 SetSmartPlayerVideoHWDecoder 设置是否用H.264硬解码播放,如硬解码不支持,自动适配到软解码
硬解码设置H.265 SetSmartPlayerVideoHevcHWDecoder 设置是否用H.265硬解码播放,如硬解码不支持,自动适配到软解码
视频画面

填充模式

SmartPlayerSetRenderScaleMode 设置视频画面的填充模式,如填充整个view、等比例填充view,如不设置,默认填充整个view
设置SurfaceView模式下render类型 SmartPlayerSetSurfaceRenderFormat 设置SurfaceView模式下(NTRenderer.CreateRenderer第二个参数传false的情况),render类型

0: RGB565格式,如不设置,默认此模式; 1: ARGB8888格式

设置SurfaceView模式下抗锯齿效果 SmartPlayerSetSurfaceAntiAlias 设置SurfaceView模式下(NTRenderer.CreateRenderer第二个参数传false的情况),抗锯齿效果,注意:抗锯齿模式开启后,可能会影像性能,请慎用
设置播放的surface SmartPlayerSetSurface 设置播放的surface,如果为null,则播放纯音频
设置视频硬解码下Mediacodec自行绘制模式 SmartPlayerSetHWRenderMode 此种模式下,硬解码兼容性和效率更好,回调YUV/RGB快照和图像等比例缩放功能将不可用
更新硬解码surface SmartPlayerUpdateHWRenderSurface 设置更新硬解码surface
音频回调 YUV/RGB SmartPlayerSetExternalRender 提供解码后YUV/RGB数据接口,供用户自己render或进一步处理(如视频分析)
Audio SmartPlayerSetExternalAudioOutput 回调audio数据到上层(供二次处理之用)
audio输出类型 SmartPlayerSetAudioOutputType 如果use_audiotrack设置为0,将会自动选择输出设备,如果设置为1,使用audiotrack模式,一对一回音消除模式下,请选用audiotrack模式
Video输出类型 NTRenderer.CreateRenderer(上层demo内) 第二个参数,如果是true,用openGLES绘制,false则用默认surfaceView
播放模式 缓冲时间设置 SmartPlayerSetBuffer 设置播放端缓存数据buffer,单位:毫秒,如不需buffer,设置为0
首屏秒开 SmartPlayerSetFastStartup 设置快速启动后,如果CDN缓存GOP,实现首屏秒开
低延迟模式 SmartPlayerSetLowLatencyMode 针对类似于直播娃娃机等期待超低延迟的使用场景,超低延迟播放模式下,延迟可达到200~400ms
快速切换URL SmartPlayerSwitchPlaybackUrl 快速切换播放url,快速切换时,只换播放source部分,适用于不同数据流之间,快速切换(如娃娃机双摄像头切换或高低分辨率流切换)
RTSP TCP/UDP模式设置 SmartPlayerSetRTSPTcpMode 设置RTSP TCP/UDP模式,如不设置,默认UDP模式
RTSP超时时间设置 SmartPlayerSetRTSPTimeout 设置RTSP超时时间,timeout单位为秒,必须大于0
设置RTSP TCP/UDP自动切换 SmartPlayerSetRTSPAutoSwitchTcpUdp 对于RTSP来说,有些可能支持rtp over udp方式,有些可能支持使用rtp over tcp方式

为了方便使用,有些场景下可以开启自动尝试切换开关, 打开后如果udp无法播放,sdk会自动尝试tcp, 如果tcp方式播放不了,sdk会自动尝试udp.

设置RTSP用户名和密码 SetRTSPAuthenticationInfo 如果RTSP URL已包含用户名和密码, 此接口设置的用户名和密码将无效. 就是说要用这个接口设置的用户名和密码去做认证, RTSP URL不能包含用户名和密码.
实时静音 SmartPlayerSetMute 实时静音
设置播放音量 SmartPlayerSetAudioVolume 播放端音量实时调节,范围[0,100],0时为静音,100为原始流数据最大音量
设置是否禁用 Enhanced

 RTMP

DisableEnhancedRTMP disable enhanced RTMP, SDK默认是开启enhanced RTMP的
实时截图 CaptureImage 支持JPEG和PNG两种格式
视频镜像旋转 旋转 SmartPlayerSetRotation 设置顺时针旋转, 注意除了0度之外, 其他角度都会额外消耗性能,当前支持 0度,90度, 180度, 270度 旋转
水平反转 SmartPlayerSetFlipHorizontal 设置视频水平反转
垂直反转 SmartPlayerSetFlipVertical 设置视频垂直反转
设置URL SmartPlayerSetUrl 设置需要播放或录像的RTMP/RTSP url
开始播放 SmartPlayerStartPlay 开始播放RTSP/RTMP流
停止播放 SmartPlayerStopPlay 停止播放RTSP/RTMP流
关闭播放实例 SmartPlayerClose 结束时必须调用close接口释放资源

功能支持

  • 音频:AAC/Speex(RTMP)/PCMA/PCMU;
  • 视频:H.264、H.265;
  • 播放协议:RTSP|RTMP;
  • 支持纯音频、纯视频、音视频播放;
  • 支持多实例播放;
  • 支持软解码,特定机型硬解码;
  • 支持RTSP TCP、UDP模式设置;
  • 支持RTSP TCP、UDP模式自动切换;
  • 支持RTSP超时时间设置,单位:秒;
  • 支持buffer时间设置,单位:毫秒;
  • 支持超低延迟模式;
  • 支持断网自动重连、视频追赶,支持buffer状态等回调;
  • 支持视频view实时旋转(0° 90° 180° 270°);
  • 支持视频view水平反转、垂直反转;
  • 支持Surfaceview/OpenGL ES/TextureView绘制;
  • 支持视频画面填充模式设置;
  • 音频支持AudioTrack、OpenSL ES模式;
  • 支持jpeg、png实时截图;
  • 支持实时音量调节;
  • 支持解码前音视频数据回调;
  • 支持解码后YUV/RGB数据回调;
  • 支持Enhanced RTMP;
  • 支持扩展录像功能;
  • 支持Android 5.1及以上版本。

接口调用详解

本文以大牛直播SDK Android平台SmartPlayerV2为例,播放之前,设置初始化参数配置(软解还是硬解、buffer time等)和需要播放的RTSP或RTMP URL,点开始播放即可。

onCreate()时,先new SmartPlayerJniV2():

/*
 * SmartPlayer.java
 * Author: daniusdk.com
 */
@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_smart_player);
	
	...

    libPlayer = new SmartPlayerJniV2();
    myContext = this.getApplicationContext();
}

开始播放、停止播放实现,开始播放的时候,调用InitAndSetConfig(),完成常规参数初始化,然后调用仅播放相关的其他接口。

btnStartStopPlayback.setOnClickListener(new Button.OnClickListener() {

	// @Override
	public void onClick(View v) {

		if (isPlaying) {
			Log.i(TAG, "Stop playback stream++");

			int iRet = libPlayer.SmartPlayerStopPlay(playerHandle);

			if (iRet != 0) {
				Log.e(TAG, "Call SmartPlayerStopPlay failed..");
				return;
			}

			btnHardwareDecoder.setEnabled(true);
			btnLowLatency.setEnabled(true);

			if (!isRecording) {
				btnPopInputUrl.setEnabled(true);
				btnSetPlayBuffer.setEnabled(true);
				btnFastStartup.setEnabled(true);

				btnRecoderMgr.setEnabled(true);
				libPlayer.SmartPlayerClose(playerHandle);
				playerHandle = 0;
			}

			isPlaying = false;
			btnStartStopPlayback.setText("开始播放 ");

			if (is_enable_hardware_render_mode && sSurfaceView != null) {
				sSurfaceView.setVisibility(View.GONE);
				sSurfaceView.setVisibility(View.VISIBLE);
			}

			Log.i(TAG, "Stop playback stream--");
		} else {
			Log.i(TAG, "Start playback stream++");

			if (!isRecording) {
				InitAndSetConfig();
			}

			// 如果第二个参数设置为null,则播放纯音频
			libPlayer.SmartPlayerSetSurface(playerHandle, sSurfaceView);

			libPlayer.SmartPlayerSetRenderScaleMode(playerHandle, 1);

			//int render_format = 1;
			//libPlayer.SmartPlayerSetSurfaceRenderFormat(playerHandle, render_format);

			//int is_enable_anti_alias = 1;
			//libPlayer.SmartPlayerSetSurfaceAntiAlias(playerHandle, is_enable_anti_alias);

			if (isHardwareDecoder && is_enable_hardware_render_mode) {
				libPlayer.SmartPlayerSetHWRenderMode(playerHandle, 1);
			}

			// External Render test
			//libPlayer.SmartPlayerSetExternalRender(playerHandle, new RGBAExternalRender(imageSavePath));
			//libPlayer.SmartPlayerSetExternalRender(playerHandle, new I420ExternalRender(imageSavePath));

			libPlayer.SmartPlayerSetUserDataCallback(playerHandle, new UserDataCallback());
			//libPlayer.SmartPlayerSetSEIDataCallback(playerHandle, new SEIDataCallback());

			libPlayer.SmartPlayerSetAudioOutputType(playerHandle, 1);

			if (isMute) {
				libPlayer.SmartPlayerSetMute(playerHandle, isMute ? 1
						: 0);
			}

			if (isHardwareDecoder) {
				int isSupportHevcHwDecoder = libPlayer.SetSmartPlayerVideoHevcHWDecoder(playerHandle, 1);

				int isSupportH264HwDecoder = libPlayer
						.SetSmartPlayerVideoHWDecoder(playerHandle, 1);

				Log.i(TAG, "isSupportH264HwDecoder: " + isSupportH264HwDecoder + ", isSupportHevcHwDecoder: " + isSupportHevcHwDecoder);
			}

			libPlayer.SmartPlayerSetLowLatencyMode(playerHandle, isLowLatency ? 1
					: 0);

			libPlayer.SmartPlayerSetFlipVertical(playerHandle, is_flip_vertical ? 1 : 0);

			libPlayer.SmartPlayerSetFlipHorizontal(playerHandle, is_flip_horizontal ? 1 : 0);

			libPlayer.SmartPlayerSetRotation(playerHandle, rotate_degrees);

			libPlayer.SmartPlayerSetAudioVolume(playerHandle, curAudioVolume);

			int iPlaybackRet = libPlayer
					.SmartPlayerStartPlay(playerHandle);

			if (iPlaybackRet != 0) {
				Log.e(TAG, "Call SmartPlayerStartPlay failed..");
				return;
			}

			btnStartStopPlayback.setText("停止播放 ");

			btnPopInputUrl.setEnabled(false);
			btnPopInputKey.setEnabled(false);
			btnSetPlayBuffer.setEnabled(false);
			btnLowLatency.setEnabled(false);
			btnFastStartup.setEnabled(false);
			btnRecoderMgr.setEnabled(false);

			isPlaying = true;
			Log.i(TAG, "Start playback stream--");
		}
	}
});

由于RTSP、RTMP播放模块,除了常规的直播播放外,也可能录像、或者实时拉流转发到RTMP服务器或轻量级RTSP服务,所以,和录像、转发相关的播放端基础参数配置,放到InitAndSetConfig()实现:

private void InitAndSetConfig() {
	playerHandle = libPlayer.SmartPlayerOpen(myContext);

	if (playerHandle == 0) {
		Log.e(TAG, "surfaceHandle with nil..");
		return;
	}

	libPlayer.SetSmartPlayerEventCallbackV2(playerHandle,
			new EventHandeV2());

	libPlayer.SmartPlayerSetBuffer(playerHandle, playBuffer);

	// set report download speed(默认2秒一次回调 用户可自行调整report间隔)
	libPlayer.SmartPlayerSetReportDownloadSpeed(playerHandle, 1, 2);

	libPlayer.SmartPlayerSetFastStartup(playerHandle, isFastStartup ? 1 : 0);

	//设置RTSP超时时间
	int rtsp_timeout = 10;
	libPlayer.SmartPlayerSetRTSPTimeout(playerHandle, rtsp_timeout);

	//设置RTSP TCP/UDP模式自动切换
	int is_auto_switch_tcp_udp = 1;
	libPlayer.SmartPlayerSetRTSPAutoSwitchTcpUdp(playerHandle, is_auto_switch_tcp_udp);

	libPlayer.SmartPlayerSaveImageFlag(playerHandle, 1);

	// It only used when playback RTSP stream..
	// libPlayer.SmartPlayerSetRTSPTcpMode(playerHandle, 1);

	// playbackUrl = "rtmp://localhost:1935/live/stream1";

	if (playbackUrl == null) {
		Log.e(TAG, "playback URL with NULL...");
		return;
	}

	libPlayer.SmartPlayerSetUrl(playerHandle, playbackUrl);
	// try_set_rtsp_url(playbackUrl);
}

EventHandle播放端事件回调处理,是底层状态反馈非常重要的媒介,除了网络状态、buffering状态回调外、还有录像状态、快照状态等回调:

class EventHandeV2 implements NTSmartEventCallbackV2 {
	@Override
	public void onNTSmartEventCallbackV2(long handle, int id, long param1,
										 long param2, String param3, String param4, Object param5) {

		String player_event = "";

		switch (id) {
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STARTED:
				player_event = "开始..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTING:
				player_event = "连接中..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED:
				player_event = "连接失败..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTED:
				player_event = "连接成功..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED:
				player_event = "连接断开..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STOP:
				player_event = "停止播放..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO:
				player_event = "分辨率信息: width: " + param1 + ", height: " + param2;
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED:
				player_event = "收不到媒体数据,可能是url错误..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL:
				player_event = "切换播放URL..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE:
				player_event = "快照: " + param1 + " 路径:" + param3;

				if (param1 == 0)
					player_event = player_event + ", 截取快照成功";
				 else
					player_event = player_event + ", 截取快照失败";

				if (param4 != null && !param4.isEmpty())
					player_event += (", user data:" + param4);

				break;

			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE:
				player_event = "[record]开始一个新的录像文件 : " + param3;
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED:
				player_event = "[record]已生成一个录像文件 : " + param3;
				break;

			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_START_BUFFERING:
				Log.i(TAG, "Start Buffering");
				break;

			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_BUFFERING:
				Log.i(TAG, "Buffering:" + param1 + "%");
				break;

			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STOP_BUFFERING:
				Log.i(TAG, "Stop Buffering");
				break;

			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_DOWNLOAD_SPEED:
				player_event = "download_speed:" + param1 + "Byte/s" + ", "
						+ (param1 * 8 / 1000) + "kbps" + ", " + (param1 / 1024)
						+ "KB/s";
				break;

			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RTSP_STATUS_CODE:
				Log.e(TAG, "RTSP error code received, please make sure username/password is correct, error code:" + param1);
				player_event = "RTSP error code:" + param1;
				break;
		}

		if (player_event.length() > 0) {
			Log.i(TAG, player_event);
			Message message = new Message();
			message.what = PLAYER_EVENT_MSG;
			message.obj = player_event;
			handler.sendMessage(message);
		}
	}
}

如果RTSP、RTMP流需要录像:

btnStartStopRecorder.setOnClickListener(new Button.OnClickListener() {

	// @Override
	public void onClick(View v) {

		if (isRecording) {

			int iRet = libPlayer.SmartPlayerStopRecorder(playerHandle);

			if (iRet != 0) {
				Log.e(TAG, "Call SmartPlayerStopRecorder failed..");
				return;
			}

			if (!isPlaying) {
				btnPopInputUrl.setEnabled(true);
				btnSetPlayBuffer.setEnabled(true);
				btnFastStartup.setEnabled(true);
				btnRecoderMgr.setEnabled(true);

				libPlayer.SmartPlayerClose(playerHandle);
				playerHandle = 0;
			}

			btnStartStopRecorder.setText(" 开始录像");

			isRecording = false;
		} else {
			Log.i(TAG, "onClick start recorder..");

			if (!isPlaying) {
				InitAndSetConfig();
			}

			ConfigRecorderFunction();

			int startRet = libPlayer.SmartPlayerStartRecorder(playerHandle);

			if (startRet != 0) {
				Log.e(TAG, "Failed to start recorder.");
				return;
			}

			btnPopInputUrl.setEnabled(false);
			btnSetPlayBuffer.setEnabled(false);
			btnFastStartup.setEnabled(false);
			btnRecoderMgr.setEnabled(false);

			isRecording = true;
			btnStartStopRecorder.setText("停止录像");
		}
	}
});

其中,录像参数配置选项设置如下,除了下面演示接口外,还可以设置仅录视频或音频:

void ConfigRecorderFunction() {
	if (libPlayer != null) {
		int is_rec_trans_code = 1;
		libPlayer.SmartPlayerSetRecorderAudioTranscodeAAC(playerHandle, is_rec_trans_code);

		if (recDir != null && !recDir.isEmpty()) {
			int ret = libPlayer.SmartPlayerCreateFileDirectory(recDir);
			if (0 == ret) {
				if (0 != libPlayer.SmartPlayerSetRecorderDirectory(
						playerHandle, recDir)) {
					Log.e(TAG, "Set recoder dir failed , path:" + recDir);
					return;
				}

				if (0 != libPlayer.SmartPlayerSetRecorderFileMaxSize(
						playerHandle, 200)) {
					Log.e(TAG,
							"SmartPublisherSetRecorderFileMaxSize failed.");
					return;
				}

			} else {
				Log.e(TAG, "Create recorder dir failed, path:" + recDir);
			}
		}
	}
}

如需播放过程中实时截图:

btnCaptureImage.setOnClickListener(new Button.OnClickListener() {
	@SuppressLint("SimpleDateFormat")
	public void onClick(View v) {
		if (0 == playerHandle)
			return;

		if (null == capture_image_date_format_)
			capture_image_date_format_ = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS");

		String timestamp = capture_image_date_format_.format(new Date());
		String imageFileName = timestamp;

		String image_path = imageSavePath + "/" + imageFileName;

		int quality;
		boolean is_jpeg = true;
		if (is_jpeg) {
			image_path += ".jpeg";
			quality = 100;
		}
		else {
			image_path += ".png";
			quality = 100;
		}

		int capture_ret = libPlayer.CaptureImage(playerHandle,is_jpeg?0:1, quality, image_path, "test cix");
		Log.i(TAG, "capture image ret:" + capture_ret + ", file:" + image_path);
	}
});

如需对视频view做水平、垂直翻转或旋转:

btnFlipVertical.setOnClickListener(new Button.OnClickListener() {
	public void onClick(View v) {
		is_flip_vertical = !is_flip_vertical;

		if (is_flip_vertical) {
			btnFlipVertical.setText("取消反转");
		} else {
			btnFlipVertical.setText("垂直反转");
		}

		if (playerHandle != 0) {
			libPlayer.SmartPlayerSetFlipVertical(playerHandle,
					is_flip_vertical ? 1 : 0);
		}
	}
});

btnFlipHorizontal.setOnClickListener(new Button.OnClickListener() {
	public void onClick(View v) {
		is_flip_horizontal = !is_flip_horizontal;

		if (is_flip_horizontal) {
			btnFlipHorizontal.setText("取消反转");
		} else {
			btnFlipHorizontal.setText("水平反转");
		}

		if (playerHandle != 0) {
			libPlayer.SmartPlayerSetFlipHorizontal(playerHandle,
					is_flip_horizontal ? 1 : 0);
		}
	}
});

btnRotation.setOnClickListener(new Button.OnClickListener() {
	public void onClick(View v) {

		rotate_degrees += 90;
		rotate_degrees = rotate_degrees % 360;

		if (0 == rotate_degrees) {
			btnRotation.setText("旋转90度");
		} else if (90 == rotate_degrees) {
			btnRotation.setText("旋转180度");
		} else if (180 == rotate_degrees) {
			btnRotation.setText("旋转270度");
		} else if (270 == rotate_degrees) {
			btnRotation.setText("不旋转");
		}

		if (playerHandle != 0) {
			libPlayer.SmartPlayerSetRotation(playerHandle,
					rotate_degrees);
		}
	}
});

onDestroy() 的时候,停掉播放、录像、释放播放端实例句柄:

@Override
protected void onDestroy() {
	Log.i(TAG, "Run into activity destory++");

	if (playerHandle != 0) {
		if (isPlaying) {
			libPlayer.SmartPlayerStopPlay(playerHandle);
		}

		if (isRecording) {
			libPlayer.SmartPlayerStopRecorder(playerHandle);
		}

		libPlayer.SmartPlayerClose(playerHandle);
		playerHandle = 0;
	}
	super.onDestroy();
	finish();
	System.exit(0);
}

以上是大概的流程,如果需要播放多实例,可以做个简单的封装,多实例效果如下:

编辑

LibPlayerWrapper.java参考封装代码如下,如需额外功能,只要按照设计框架,添加进去即可:

/*
 * LibPlayerWrapper.java.java
 * Author: daniusdk.com
 */
package com.daniulive.smartplayer;

import android.content.Context;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.View;

import com.eventhandle.NTSmartEventCallbackV2;
import java.lang.ref.WeakReference;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LibPlayerWrapper {
    private static String TAG = "NTLogLibPlayerW";
    private static final int OK = 0;

    private WeakReference<Context> context_;
    private final ReadWriteLock rw_lock_ = new ReentrantReadWriteLock(true);
    private final java.util.concurrent.locks.Lock write_lock_ = rw_lock_.writeLock();
    private final java.util.concurrent.locks.Lock read_lock_ = rw_lock_.readLock();

    private SmartPlayerJniV2 lib_player_;
    private volatile long native_handle_;
    private View view_;

    private volatile boolean is_playing_;
    private volatile boolean is_recording_;

    private WeakReference<EventListener> event_listener_;

    public LibPlayerWrapper(SmartPlayerJniV2 lib_player, Context context, EventListener listener) {
        if (!empty())
            throw new IllegalStateException("it is not empty");

        if (null == lib_player)
            throw new NullPointerException("lib_player is null");

        this.lib_player_ = lib_player;

        if (context != null)
            this.context_ = new WeakReference<>(context);

        if (listener == null ) {
            this.event_listener_ = null;
        }
        else {
            this.event_listener_ = new WeakReference<>(listener);
        }
    }

    private void clear_all_playing_flags() {
        this.is_playing_ = false;
        this.is_recording_ = false;
    }

    public void set(long handle) {
        if (!empty())
            throw new IllegalStateException("it is not empty");

        write_lock_.lock();
        try {
            clear_all_playing_flags();
            this.native_handle_ = handle;
        } finally {
            write_lock_.unlock();
        }

        Log.i(TAG, "set native_handle:" + handle);
    }

    public void SetView(View view) {
        Log.i(TAG, "SetView: " + view);
        this.view_ = view;
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            if (check_native_handle()) {
                if(is_playing()) {
                    lib_player_.SmartPlayerStopPlay(get());
                    this.is_playing_ = false;
                }

                if(is_recording()) {
                    lib_player_.SmartPlayerStopRecorder(get());
                    this.is_recording_ = false;
                }

                lib_player_.SmartPlayerClose(this.native_handle_);
                Log.i(TAG, "finalize close handle:" + this.native_handle_);
                this.native_handle_ = 0;
            }
        }catch (Exception e) {

        }

        super.finalize();
    }

    public void release() {
        if (empty())
            return;

        if(is_playing())
            StopPlayer();

        if (is_recording())
            StopRecorder();

        long handle;
        write_lock_.lock();
        try {
            handle = this.native_handle_;
            this.native_handle_ = 0;
            clear_all_playing_flags();
        } finally {
            write_lock_.unlock();
        }

        if (lib_player_ != null && handle != 0)
            lib_player_.SmartPlayerClose(handle);
    }

    public boolean try_release() {
        if (empty())
            return false;

        if (is_player_running()) {
            Log.i(TAG, "try_release it is running, native_handle:" + get());
            return false;
        }

        long handle;
        write_lock_.lock();
        try {
            if (is_player_running())
                return false;

            handle = this.native_handle_;
            this.native_handle_ = 0;
        } finally {
            write_lock_.unlock();
        }

        if (lib_player_ != null && handle != 0)
            lib_player_.SmartPlayerClose(handle);

        return true;
    }

    public final boolean empty() { return 0 == this.native_handle_; }

    public final long get() { return this.native_handle_; }

    public View get_view() {return this.view_;}

    public final boolean check_native_handle() {
        return this.lib_player_ != null && this.native_handle_ != 0;
    }

    public final boolean is_playing() { return is_playing_; }

    public final boolean is_recording() { return is_recording_; }

    public final boolean is_player_running() { return is_playing_ || is_recording_; }

    private boolean isValidRtspOrRtmpUrl(String url) {
        if (url == null || url.isEmpty()) {
            return false;
        }
        return url.trim().startsWith("rtsp://") || url.startsWith("rtmp://");
    }

    private EventListener getListener() {
        if ( this.event_listener_ == null )
            return null;

        return this.event_listener_.get();
    }

    protected final Context application_context() {
        if (null == context_)
            return null;

        return context_.get();
    }

    public boolean OpenPlayerHandle(String playback_url, int play_buffer, int is_using_tcp) {

        if (check_native_handle())
            return true;

        if(!isValidRtspOrRtmpUrl(playback_url))
            return false;

        long handle = lib_player_.SmartPlayerOpen(application_context());
        if (0==handle) {
            Log.e(TAG, "sdk open failed!");
            return false;
        }

        lib_player_.SetSmartPlayerEventCallbackV2(handle, new EventHandleV2());

        lib_player_.SmartPlayerSetBuffer(handle, play_buffer);

        // set report download speed(默认2秒一次回调 用户可自行调整report间隔)
        lib_player_.SmartPlayerSetReportDownloadSpeed(handle, 1, 4);

        boolean isFastStartup = true;
        lib_player_.SmartPlayerSetFastStartup(handle, isFastStartup ? 1 : 0);

        //设置RTSP超时时间
        int rtsp_timeout = 10;
        lib_player_.SmartPlayerSetRTSPTimeout(handle, rtsp_timeout);

        //设置RTSP TCP/UDP模式自动切换
        int is_auto_switch_tcp_udp = 1;
        lib_player_.SmartPlayerSetRTSPAutoSwitchTcpUdp(handle, is_auto_switch_tcp_udp);

        lib_player_.SmartPlayerSaveImageFlag(handle, 1);

        // It only used when playback RTSP stream..
        lib_player_.SmartPlayerSetRTSPTcpMode(handle, is_using_tcp);

        lib_player_.DisableEnhancedRTMP(handle, 0);

        lib_player_.SmartPlayerSetUrl(handle, playback_url);

        set(handle);

        return true;
    }

    private void SetPlayerParam(boolean is_hardware_decoder, boolean is_enable_hardware_render_mode, boolean is_mute)
    {
         Surface surface = null;
         int surface_codec_media_color_format = 0;

         if (view_ != null && view_ instanceof SurfaceView && ((SurfaceView) view_).getHolder() != null)
             surface = ((SurfaceView) view_).getHolder().getSurface();

         lib_player_.SetSurface(get(), surface, surface_codec_media_color_format, 0, 0);

        lib_player_.SmartPlayerSetRenderScaleMode(get(), 1);

        //int render_format = 1;
        //lib_player.SmartPlayerSetSurfaceRenderFormat(handle, render_format);

        //int is_enable_anti_alias = 1;
        //lib_player.SmartPlayerSetSurfaceAntiAlias(handle, is_enable_anti_alias);

        if (is_hardware_decoder && is_enable_hardware_render_mode) {
            lib_player_.SmartPlayerSetHWRenderMode(get(), 1);
        }

        lib_player_.SmartPlayerSetAudioOutputType(get(), 1);

        lib_player_.SmartPlayerSetMute(get(), is_mute ? 1 : 0);

        if (is_hardware_decoder) {
            int isSupportHevcHwDecoder = lib_player_.SetSmartPlayerVideoHevcHWDecoder(get(), 1);

            int isSupportH264HwDecoder = lib_player_.SetSmartPlayerVideoHWDecoder(get(), 1);

            Log.i(TAG, "isSupportH264HwDecoder: " + isSupportH264HwDecoder + ", isSupportHevcHwDecoder: " + isSupportHevcHwDecoder);
        }

        boolean isLowLatency = true;
        lib_player_.SmartPlayerSetLowLatencyMode(get(), isLowLatency ? 1 : 0);

        boolean is_flip_vertical = false;
        lib_player_.SmartPlayerSetFlipVertical(get(), is_flip_vertical ? 1 : 0);

        boolean is_flip_horizontal = false;
        lib_player_.SmartPlayerSetFlipHorizontal(get(), is_flip_horizontal ? 1 : 0);

        int rotate_degrees = 0;
        lib_player_.SmartPlayerSetRotation(get(), rotate_degrees);

        int curAudioVolume = 100;
        lib_player_.SmartPlayerSetAudioVolume(get(), curAudioVolume);
    }

    class EventHandleV2 implements NTSmartEventCallbackV2 {
        @Override
        public void onNTSmartEventCallbackV2(long handle, int id, long param1,
                                             long param2, String param3, String param4, Object param5) {

            if(event_listener_.get() != null)
            {
                event_listener_.get().onPlayerEventCallback(handle, id, param1, param2, param3, param4, param5);
            }
        }
    }

    public boolean SetMute(boolean is_mute) {
        if (!check_native_handle())
            return false;

        return OK == lib_player_.SmartPlayerSetMute(get(), is_mute? 1 : 0);
    }

    public boolean SetInputAudioVolume(int volume) {
        if (!check_native_handle())
            return false;

        return OK == lib_player_.SmartPlayerSetAudioVolume(get(), volume);
    }

    public boolean CaptureImage(int compress_format, int quality, String file_name, String user_data_string) {
        if (!check_native_handle())
            return false;

        return OK == lib_player_.CaptureImage(get(), compress_format, quality, file_name, user_data_string);
    }

    public boolean StartPlayer(boolean is_hardware_decoder, boolean is_enable_hardware_render_mode, boolean is_mute) {
        if (is_playing()) {
            Log.e(TAG, "already playing, native_handle:" + get());
            return false;
        }

        SetPlayerParam(is_hardware_decoder, is_enable_hardware_render_mode, is_mute);

        int ret = lib_player_.SmartPlayerStartPlay(get());
        if (ret != OK) {
            Log.e(TAG, "call StartPlay failed, native_handle:" + get() + ", ret:" + ret);
            return false;
        }

        write_lock_.lock();
        try {
            this.is_playing_ = true;
        } finally {
            write_lock_.unlock();
        }

        Log.i(TAG, "call StartPlayer OK, native_handle:" + get());
        return true;
    }

    public boolean StopPlayer() {
        if (!check_native_handle())
            return false;

        if (!is_playing()) {
            Log.w(TAG, "it's not playing, native_handle:" + get());
            return false;
        }

        boolean is_need_call = false;
        write_lock_.lock();
        try {
            if (this.is_playing_) {
                this.is_playing_ = false;
                is_need_call = true;
            }
        } finally {
            write_lock_.unlock();
        }

        if (is_need_call)
            lib_player_.SmartPlayerStopPlay(get());

        return true;
    }

    public boolean ConfigRecorderParam(String rec_dir, int file_max_size, int is_transcode_aac,
                                       int is_record_video, int is_record_audio) {

        if(!check_native_handle())
            return false;

        if (null == rec_dir || rec_dir.isEmpty())
            return false;

        int ret = lib_player_.SmartPlayerCreateFileDirectory(rec_dir);
        if (ret != 0) {
            Log.e(TAG, "Create record dir failed, path:" + rec_dir);
            return false;
        }

        if (lib_player_.SmartPlayerSetRecorderDirectory(get(), rec_dir) != 0) {
            Log.e(TAG, "Set record dir failed , path:" + rec_dir);
            return false;
        }

        if (lib_player_.SmartPlayerSetRecorderFileMaxSize(get(),file_max_size) != 0) {
            Log.e(TAG, "SmartPlayerSetRecorderFileMaxSize failed.");
            return false;
        }

        lib_player_.SmartPlayerSetRecorderAudioTranscodeAAC(get(), is_transcode_aac);

        // 更细粒度控制录像的, 一般情况无需调用
        lib_player_.SmartPlayerSetRecorderVideo(get(), is_record_video);
        lib_player_.SmartPlayerSetRecorderAudio(get(), is_record_audio);
        return true;
    }

    public boolean StartRecorder() {

        if (is_recording()) {
            Log.e(TAG, "already recording, native_handle:" + get());
            return false;
        }

        int ret = lib_player_.SmartPlayerStartRecorder(get());
        if (ret != OK) {
            Log.e(TAG, "call SmartPlayerStartRecorder failed, native_handle:" + get() + ", ret:" + ret);
            return false;
        }

        write_lock_.lock();
        try {
            this.is_recording_ = true;
        } finally {
            write_lock_.unlock();
        }

        Log.i(TAG, "call SmartPlayerStartRecorder OK, native_handle:" + get());
        return true;
    }

    public boolean StopRecorder() {
        if (!check_native_handle())
            return false;

        if (!is_recording()) {
            Log.w(TAG, "it's not recording, native_handle:" + get());
            return false;
        }

        boolean is_need_call = false;
        write_lock_.lock();
        try {
            if (this.is_recording_) {
                this.is_recording_ = false;
                is_need_call = true;
            }
        } finally {
            write_lock_.unlock();
        }

        if (is_need_call)
            lib_player_.SmartPlayerStopRecorder(get());

        return true;
    }

    private static boolean is_null_or_empty(String val) {
        return null == val || val.isEmpty();
    }
}

总结

以上是Android平台RTSP、RTMP直播播放模块对接说明,在此之前,我们针对SmartPlayer做过一些技术方面的探讨,从低延迟、音视频同步处理、多实例实现、解码效率、性能占用、解码后数据对接、实时截图、录像、网络抖动处理等各个维度,做过相关的技术分享。感兴趣的开发者,可以单独跟我们探讨。

 

Android平台RTMP直播推送模块技术接入说明

技术背景

大牛直播SDK跨平台RTMP直播推送模块,始于2015年,支持Windows、Linux(x64_64架构|aarch64)、Android、iOS平台,支持采集推送摄像头、屏幕、麦克风、扬声器、编码前、编码后数据对接,功能强大,性能优异,配合大牛直播SDK的SmartPlayer播放器,轻松实现毫秒级的延迟体验,满足大多数行业的使用场景。

RTMP直播推送模块数据源,支持编码前、编码后数据对接:

  • 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型);
  • 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据)。

技术对接

 系统要求

  • SDK支持Android5.1及以上版本;
  • 支持的CPU架构:armv7, arm64, x86, x86_64。

准备工作

  • 确保SmartPublisherJniV2.java放到com.daniulive.smartpublisher包名下(可在其他包名下调用);
  • smartavengine.jar加入到工程;
  • 拷贝libSmartPublisher.so到工程;
  • AndroidManifast.xml添加相关权限:
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />

  • Load相关so:
static {  
    System.loadLibrary("SmartPublisher");
}

  • build.gradle配置32/64位库:
splits {
    abi {
        enable true
        reset()
        // Specifies a list of ABIs that Gradle should create APKs for
        include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' //select ABIs to build APKs for
        // Specify that we do not want to also generate a universal APK that includes all ABIs
        universalApk true
    }
}

  • 如需集成到自己系统测试,请用大牛直播SDK的app name,授权版按照授权app name正常使用即可;
  • 如何改app-name,strings.xml做以下修改:
<string name="app_name">SmartPublisherSDKDemo</string>

接口设计

Android 推送端SDK接口详解
调用描述 接口 接口描述
最先调用,如成功返回推送实例 SmartPublisherOpen ctx:上下文信息;

Audio_opt:

0:不推送音频;

1:推送编码前音频(PCM);

2:对接外部编码后的audio数据(AAC/PCMA/PCMU/SPEEX)

video_opt:

0:不推送视频;

1:推送编码前视频(YUV420SP/YUV420P/RGBA/ARGB);

2:推送编码后视频(H.264)

3:层叠加模式

width|height:宽高信息。

Event回调 SetSmartPublisherEventCallbackV2 设置event callback
硬编码设置 SetSmartPublisherVideoHWEncoder 检测是否支持H.264硬编码,如果返回0,则支持,否则自动采用软编码
SetSmartPublisherVideoHevcHWEncoder 检测是否支持H.265(HEVC)硬编码,如果返回0,则支持,否则自动采用软编码
SetNativeMediaNDK 设置视频硬编码是否使用 Native Media NDK, 默认是不使用, 安卓5.0以下设备不支持
SetVideoHWEncoderBitrateMode 设置视频硬编码码率控制模式

hw_bitrate_mode: -1表示使用默认值, 不设置也会使用默认值, 0:CQ, 1:VBR, 2:CBR, 3:CBR_FD

SetVideoHWEncoderComplexity 设置视频硬编码复杂度, 安卓5.0及以上支持
SetVideoHWEncoderQuality 设置视频硬编码质量, 安卓9及以上支持, 仅当硬编码器码率控制模式(BitrateMode)是CQ(constant-quality mode)时才有效
SetAVCHWEncoderProfile 设置H.264硬编码Profile, 安卓7及以上支持
SetAVCHWEncoderLevel 设置H.264硬编码Level, 这个只有在设置了Profile的情况下才有效, 安卓7及以上支持
SetVideoHWEncoderMaxBitrate 设置视频硬编码最大码率, 安卓没有相关文档说明, 所以不建议设置
水印 文字、png水印 PostLayerBitmap 通过层模式设置水印,投递层

Bitmap.Config.ARGB_888图像

视频参数配置 软编码可变码率 SmartPublisherSetSwVBRMode 设置软编码可变码率,可变码率下,相邻帧之间变化不大时码率更低
GOP间隔(关键帧) SmartPublisherSetGopInterval 设置推送端GOP间隔,一般建议在帧率的1~3倍,如不设置,用底层默认值
软编码码率设置 SmartPublisherSetSWVideoBitRate 设置软编码视频 bit-rate,最大码流一般是平均码流的2倍,如不设置,用底层计算的默认值
帧率 SmartPublisherSetFPS 设置fps,如不设置,用底层默认值
软编码视频Profile SmartPublisherSetSWVideoEncoderProfile 设置软编码模式下的video encoder profile,默认baseline profile
软编码编码速度 SmartPublisherSetSWVideoEncoderSpeed 设置软编码编码速度,设置范围(1,6),1最快,6最慢,默认是6
频设置 视频镜像 SmartPublisherSetMirror 镜像模式: 播放端和推送端本地回显方向显示一致(前置摄像头)
视频截图 实时快照 CaptureImage 截图接口, 支持JPEG和PNG两种格式
音频配置 音频编码

类型

SmartPublisherSetAudioCodecType 设置编码类型,默认AAC编码,type设置为2时,启用speex编码(码率更低)
AAC编码码率 SmartPublisherSetAudioBitRate 设置音频编码码率, 当前只对AAC编码有效
SPEEX编码质量 SmartPublisherSetSpeexEncoderQuality 设置speex编码质量,数值越大,质量越高,范围(0,10),默认8
音频处理 噪音抑制 SmartPublisherSetNoiseSuppression 噪音抑制开启后,可去除采集端背景杂音
增益控制 SmartPublisherSetAGC 设置自动增益控制,保持声音稳定
回声消除 SmartPublisherSetEchoCancellation 设置音频回音消除
实时静音 SmartPublisherSetMute 设置实时静音、取消静音
设置输入

音量

SmartPublisherSetInputAudioVolume 设置输入音量,默认是1.0,范围是[0.0, 5.0], 设置成0静音, 1音量不变
RTMP推送模式 SetRtmpPublishingType 设置rtmp publisher类型,0:live,1:record,需服务器支持
Enhanced RTMP设置 DisableEnhancedRTMP disable enhanced RTMP, SDK默认是开启enhanced RTMP的
RTMP推送URL设置 SmartPublisherSetURL 设置RTMP推送url
编码前实时视频数据 camera数据 SmartPublisherOnCaptureVideoData 对接camera回调的数据
YV12数据 SmartPublisherOnYV12Data YV12数据接口
NV21数据 SmartPublisherOnNV21Data NV21数据接口
转换接口 SmartPublisherNV21ToI420Rotate NV21转换到I420并旋转
YUV(I420) SmartPublisherOnCaptureVideoI420Data 第三方YUV(I420)接口
RGB24数据 SmartPublisherOnCaptureVideoRGB24Data RGB24接口
RGBA32数据 SmartPublisherOnCaptureVideoRGBA32Data RGBA32接口
YUV420888数据 SmartPublisherOnImageYUV420888 YUV420888接口
RGBA数据 SmartPublisherOnCaptureVideoRGBAData 第三方RGBA数据
ABGR垂直翻转数据 SmartPublisherOnCaptureVideoABGRFlip

VerticalData

ABGR flip vertical(垂直翻转) 数据(Demo中用于传递屏幕数据)
RGBA8888图像 PostLayerImageRGBA8888ByteBuffer 投递层RGBA8888图像,如果不需要Aplpha通道的话, 请使用RGBX8888接口
RGBX8888图像 PostLayerImageRGBX8888ByteBuffer 投递层RGBX8888图像
I420图像 PostLayerImageI420ByteBuffer 投递层I420图像
RGB565数据 SmartPublisherOnCaptureVideoRGB565Data RGB565 data
裁剪过的RGBA

数据

SmartPublisherOnCaptureVideoClipedRGBAData 投递裁剪过的RGBA数据
PCM数据 SmartPublisherOnPCMData 实时PCM数据
远端PCM数据

(用于回音消除)

SmartPublisherOnFarEndPCMData 实时传递远端PCM数据(可用于互动级的回音消除处理)
音频 混音 混音数据 SmartPublisherOnMixPCMData 传递PCM混音音频数据给SDK, 每10ms音频数据传入一次
编码后数据对接 编码后视频数据 SmartPublisherPostVideoEncodedData 设置编码后视频数据
编码后音频数据 SmartPublisherPostAudioEncodedData 编码后音频数据
编码后音视频数据回调 编码后音频数据回调 SmartPublisherSetAudioEncodedDataCallback 设置编码后音频数据回调
编码后视频数据回调 SmartPublisherSetVideoEncodedDataCallback 设置编码后视频数据回调
层结构设置 启用|停用视频层 EnableLayer video_opt为3时,启用或者停用视频层, 这个接口必须在StartXXX之后调用.
移除视频层 RemoveLayer 移除视频层, 这个接口必须在StartXXX之后调用.
RTMP推送 开始推送

RTMP

SmartPublisherStartPublisher 启动RTMP推送
停止推送

RTMP

SmartPublisherStopPublisher 停止RTMP推送
关闭推送实例 关闭实例 SmartPublisherClose 关闭推送实例,结束时必须调用close接口释放资源
设置授权 授权license设置 SmartPublisherSetSDKClientKey 设置授权Key,如需设置授权Key, 请确保在SmartPublisherOpen之前调用!

功能支持

  • 音频编码:AAC/SPEEX;
  • 视频编码:H.264、H.265;
  • 推流协议:RTMP;
  • [音视频]支持纯音频/纯视频/音视频推送;
  • [摄像头]支持采集过程中,前后摄像头实时切换;
  • 支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置;
  • 支持RTMP推送 live|record模式设置;
  • 支持前置摄像头镜像设置;
  • 支持软编码、特定机型硬编码;
  • 支持横屏、竖屏推送;
  • 支持Android屏幕采集推送;
  • 支持自建标准RTMP服务器或CDN;
  • 支持断网自动重连、网络状态回调;
  • 支持实时动态水印;
  • 支持实时快照;
  • 支持降噪处理、自动增益控制;
  • 支持外部编码前音视频数据对接;
  • 支持外部编码后音视频数据对接;
  • 支持RTMP扩展H.265(需设备支持H.265特定机型硬编码)和Enhanced RTMP;
  • 支持实时音量调节;
  • 支持扩展录像模块;
  • 支持Unity接口;
  • 支持H.264扩展SEI发送模块;
  • 支持Android 5.1及以上版本。

接口调用详解

本文以大牛直播SDK Android平台Camera2Demo为例,推送RTMP之前,可以先选择视频分辨率、软编还是硬编码,音频是AAC、SPEEX还是PCMA编码等基础设置,其他参数的设置,可以参考下面InitAndSetConfig()。

以Android平台Camera2对接为例,onCreate()时,想new SmartPublisherJniV2():

/*
 * MainActivity.java
 * Author: daniusdk.com
 */
@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
	
	...

	context_ = this.getApplicationContext();
	
	libPublisher = new SmartPublisherJniV2();
}

推送RTMP:

class ButtonStartPushListener implements View.OnClickListener {
	public void onClick(View v) {
		if (stream_publisher_.is_rtmp_publishing()) {
			stopPush();

			btnRTMPPusher.setText("推送RTMP");
			return;
		}

		Log.i(TAG, "onClick start push rtmp..");
		
        InitAndSetConfig();

		String rtmp_pusher_url ="rtmp://192.168.0.101:1935/hls/stream123";;


		if (!stream_publisher_.SetURL(rtmp_pusher_url))
			Log.e(TAG, "Failed to set publish stream URL..");

		boolean start_ret = stream_publisher_.StartPublisher();
		if (!start_ret) {
			stream_publisher_.try_release();
			Log.e(TAG, "Failed to start push stream..");
			return;
		}

		startAudioRecorder();
		startLayerPostThread();

		btnRTMPPusher.setText("停止推送 ");

	}
}

stopPush()实现如下:

//停止rtmp推送
private void stopPush() {
	stream_publisher_.StopPublisher();
	stream_publisher_.try_release();

	if (!stream_publisher_.is_publishing())
		stopAudioRecorder();
}

其中,InitAndSetConfig()实现如下,通过调SmartPublisherOpen()接口,生成推送实例句柄。

/*
 * MainActivity.java
 * Author: daniusdk.com
 */
private void InitAndSetConfig() {
	if (null == libPublisher)
		return;

	if (!stream_publisher_.empty())
		return;

	Log.i(TAG, "InitAndSetConfig video width: " + video_width_ + ", height" + video_height_ + " imageRotationDegree:" + cameraImageRotationDegree_);

	int audio_opt = 1;
	long handle = libPublisher.SmartPublisherOpen(context_, audio_opt, 3,  video_width_, video_height_);
	if (0==handle) {
		Log.e(TAG, "sdk open failed!");
		return;
	}

	Log.i(TAG, "publisherHandle=" + handle);

	int fps = 25;
	int gop = fps * 3;

	initialize_publisher(libPublisher, handle, video_width_, video_height_, fps, gop);

	stream_publisher_.set(libPublisher, handle);
}

对应的initialize_publisher()实现如下,设置软硬编码、帧率、关键帧间隔等。

private boolean initialize_publisher(SmartPublisherJniV2 lib_publisher, long handle, int width, int height, int fps, int gop) {
	if (null == lib_publisher) {
		Log.e(TAG, "initialize_publisher lib_publisher is null");
		return false;
	}

	if (0 == handle) {
		Log.e(TAG, "initialize_publisher handle is 0");
		return false;
	}

	if (videoEncodeType == 1) {
		int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, true);
		Log.i(TAG, "h264HWKbps: " + kbps);
		int isSupportH264HWEncoder = lib_publisher.SetSmartPublisherVideoHWEncoder(handle, kbps);
		if (isSupportH264HWEncoder == 0) {
			lib_publisher.SetNativeMediaNDK(handle, 0);
			lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
			lib_publisher.SetVideoHWEncoderQuality(handle, 39);
			lib_publisher.SetAVCHWEncoderProfile(handle, 0x08); // 0x01: Baseline, 0x02: Main, 0x08: High

			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x200); // Level 3.1
			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x400); // Level 3.2
			// lib_publisher.SetAVCHWEncoderLevel(handle, 0x800); // Level 4
			lib_publisher.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1 多数情况下,这个够用了
			//lib_publisher.SetAVCHWEncoderLevel(handle, 0x2000); // Level 4.2

			// lib_publisher.SetVideoHWEncoderMaxBitrate(handle, ((long)h264HWKbps)*1300);

			Log.i(TAG, "Great, it supports h.264 hardware encoder!");
		}
	} else if (videoEncodeType == 2) {
		int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, false);
		Log.i(TAG, "hevcHWKbps: " + kbps);
		int isSupportHevcHWEncoder = lib_publisher.SetSmartPublisherVideoHevcHWEncoder(handle, kbps);
		if (isSupportHevcHWEncoder == 0) {
			lib_publisher.SetNativeMediaNDK(handle, 0);
			lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
			lib_publisher.SetVideoHWEncoderQuality(handle, 39);

			// libPublisher.SetVideoHWEncoderMaxBitrate(handle, ((long)hevcHWKbps)*1200);

			Log.i(TAG, "Great, it supports hevc hardware encoder!");
		}
	}

	boolean is_sw_vbr_mode = true;
	//H.264 software encoder
	if (is_sw_vbr_mode) {
		int is_enable_vbr = 1;
		int video_quality = LibPublisherWrapper.estimate_video_software_quality(width, height, true);
		int vbr_max_kbps = LibPublisherWrapper.estimate_video_vbr_max_kbps(width, height, fps);
		lib_publisher.SmartPublisherSetSwVBRMode(handle, is_enable_vbr, video_quality, vbr_max_kbps);
	}

	if (is_pcma_) {
		lib_publisher.SmartPublisherSetAudioCodecType(handle, 3);
	} else {
		lib_publisher.SmartPublisherSetAudioCodecType(handle, 1);
	}

	lib_publisher.SetSmartPublisherEventCallbackV2(handle, new EventHandlerPublisherV2().set(handler_, record_executor_));

	lib_publisher.SmartPublisherSetSWVideoEncoderProfile(handle, 3);

	lib_publisher.SmartPublisherSetSWVideoEncoderSpeed(handle, 2);

	lib_publisher.SmartPublisherSetGopInterval(handle, gop);

	lib_publisher.SmartPublisherSetFPS(handle, fps);

	// lib_publisher.SmartPublisherSetSWVideoBitRate(handle, 600, 1200);

	boolean is_noise_suppression = true;
	lib_publisher.SmartPublisherSetNoiseSuppression(handle, is_noise_suppression ? 1 : 0);

	boolean is_agc = false;
	lib_publisher.SmartPublisherSetAGC(handle, is_agc ? 1 : 0);

	int echo_cancel_delay = 0;
	lib_publisher.SmartPublisherSetEchoCancellation(handle, 1, echo_cancel_delay);

	return true;
}

数据投递如下(以Camera2采集为例,如果是其他视频格式,也可以正常对接):

@Override
public void onCameraImageData(Image image) {
	....
	for (LibPublisherWrapper i : publisher_array_)
		i.PostLayerImageYUV420888ByteBuffer(0, 0, 0,
			planes[0].getBuffer(), y_offset, planes[0].getRowStride(),
			planes[1].getBuffer(), u_offset, planes[1].getRowStride(),
			planes[2].getBuffer(), v_offset, planes[2].getRowStride(), planes[1].getPixelStride(),
			w, h, 0, 0,
			scale_w, scale_h, scale_filter_mode, rotation_degree);

}

音频采集投递设计如下:

void startAudioRecorder() {
	if (audio_recorder_ != null)
		return;

	audio_recorder_ = new NTAudioRecordV2(this);

	Log.i(TAG, "startAudioRecorder call audio_recorder_.start()+++...");

	audio_recorder_callback_ = new NTAudioRecordV2CallbackImpl(stream_publisher_, null);

	audio_recorder_.AddCallback(audio_recorder_callback_);

	if (!audio_recorder_.Start(is_pcma_ ? 8000 : 44100, 1) ) {
		audio_recorder_.RemoveCallback(audio_recorder_callback_);
		audio_recorder_callback_ = null;

		audio_recorder_ = null;

		Log.e(TAG, "startAudioRecorder start failed.");
	}
	else {
		Log.i(TAG, "startAudioRecorder call audio_recorder_.start() OK---...");
	}
}

void stopAudioRecorder() {
	if (null == audio_recorder_)
		return;

	Log.i(TAG, "stopAudioRecorder+++");

	audio_recorder_.Stop();

	if (audio_recorder_callback_ != null) {
		audio_recorder_.RemoveCallback(audio_recorder_callback_);
		audio_recorder_callback_ = null;
	}

	audio_recorder_ = null;

	Log.i(TAG, "stopAudioRecorder---");
}

回调Audio数据的地方,直接投递出去:

private static class NTAudioRecordV2CallbackImpl implements NTAudioRecordV2Callback {
	private WeakReference<LibPublisherWrapper> publisher_0_;

	public NTAudioRecordV2CallbackImpl(LibPublisherWrapper publisher_0) {
		if (publisher_0 != null)
			publisher_0_ = new WeakReference<>(publisher_0);
	}

	private final LibPublisherWrapper get_publisher_0() {
		if (publisher_0_ !=null)
			return publisher_0_.get();

		return null;
	}

	@Override
	public void onNTAudioRecordV2Frame(ByteBuffer data, int size, int sampleRate, int channel, int per_channel_sample_number) {

		LibPublisherWrapper publisher_0 = get_publisher_0();
		if (publisher_0 != null)
			publisher_0.OnPCMData(data, size, sampleRate, channel, per_channel_sample_number);
	}
}

图层投递设计如下,图层投递的时候,可设置是否添加文字、图片动态水印:

private void startLayerPostThread() {
	if (layer_post_thread_ != null)
		return;

	layer_post_thread_ = new LayerPostThread(this.context_, publisher_array_);
	layer_post_thread_.start_post();
	update_layer_post_video_size();
	layer_post_thread_.enableText(isHasTextWatermark());
	layer_post_thread_.enablePicture(isHasPictureWatermark());
}

private void update_layer_post_video_size() {
	if (null == layer_post_thread_)
		return;

	int w, h;
	int degree = cameraImageRotationDegree_;
	if (degree < 0 ) {
		w = 0;
		h = 0;
	} else if (90 == degree || 270 == degree) {
		w = video_height_;
		h = video_width_;
	}else {
		w = video_width_;
		h = video_height_;
	}

	layer_post_thread_.update_video_size(w, h);
}

private void stopLayerPostThread() {
	if (layer_post_thread_ != null) {
		layer_post_thread_.stop_post();
		layer_post_thread_ = null;
	}
}

如需摄像头快照,调用以下逻辑实现即可:

class ButtonCaptureImageListener implements View.OnClickListener {
	public void onClick(View v) {
		if (null == snap_shot_impl_) {
			snap_shot_impl_ = new SnapShotImpl(image_path_, context_, handler_, libPublisher, snap_shot_publisher_);
			snap_shot_impl_.start();
		}

		startLayerPostThread();
		snap_shot_impl_.set_layer_post_thread(layer_post_thread_);

		snap_shot_impl_.capture();
	}
}

如需集成录像模块,开始录像、停止录像设计如下:

class ButtonStartRecorderListener implements View.OnClickListener {
	public void onClick(View v) {
		if (layer_post_thread_ != null)
			layer_post_thread_.update_layers();

		if (stream_publisher_.is_recording()) {
			stopRecorder();

			if (stream_publisher_.empty())
				ConfigControlEnable(true);

			btnStartRecorder.setText("实时录像");
			btnPauseRecorder.setText("暂停录像");
			btnPauseRecorder.setEnabled(false);
			isPauseRecording = true;
			return;
		}

		Log.i(TAG, "onClick start recorder..");

		InitAndSetConfig();

		ConfigRecorderParam();

		boolean start_ret = stream_publisher_.StartRecorder();
		if (!start_ret) {
			stream_publisher_.try_release();
			Log.e(TAG, "Failed to start recorder.");
			return;
		}

		startAudioRecorder();
		ConfigControlEnable(false);

		startLayerPostThread();

		btnStartRecorder.setText("停止录像");
		btnPauseRecorder.setEnabled(true);
		isPauseRecording = true;
	}
}

录像参数配置实现如下:

void ConfigRecorderParam() {
	if (null == libPublisher)
		return;

	if (null == recDir || recDir.isEmpty())
		return;

	int ret = libPublisher.SmartPublisherCreateFileDirectory(recDir);
	if (ret != 0) {
		Log.e(TAG, "Create record dir failed, path:" + recDir);
		return;
	}

	if (!stream_publisher_.SetRecorderDirectory(recDir)) {
		Log.e(TAG, "Set record dir failed , path:" + recDir);
		return;
	}

	// 更细粒度控制录像的, 一般情况无需调用
	//libPublisher.SmartPublisherSetRecorderAudio(publisherHandle, 0);
	//libPublisher.SmartPublisherSetRecorderVideo(publisherHandle, 0);

	if (!stream_publisher_.SetRecorderFileMaxSize(200)) {
		Log.e(TAG, "SmartPublisherSetRecorderFileMaxSize failed.");
		return;
	}
}

暂停录像、恢复录像设计如下:

class ButtonPauseRecorderListener implements View.OnClickListener {
	public void onClick(View v) {
		if (stream_publisher_.is_recording()) {
			if (isPauseRecording) {
				boolean ret = stream_publisher_.PauseRecorder(true);
				if (ret) {
					isPauseRecording = false;
					btnPauseRecorder.setText("恢复录像");
				} else {
					Log.e(TAG, "Pause recorder failed..");
				}
			} else {
				boolean ret = stream_publisher_.PauseRecorder(false);
				if (ret) {
					isPauseRecording = true;
					btnPauseRecorder.setText("暂停录像");
				} else {
					Log.e(TAG, "Resume recorder failed..");
				}
			}
		}
	}
}

Event回调实现如下:

private static class EventHandlerPublisherV2 implements NTSmartEventCallbackV2 {
	@Override
	public void onNTSmartEventCallbackV2(long handle, int id, long param1, long param2, String param3, String param4, Object param5) {

		Log.i(TAG, "EventHandeV2: handle=" + handle + " id:" + id);

		String publisher_event = "";

		switch (id) {
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_STARTED:
				publisher_event = "开始..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTING:
				publisher_event = "连接中..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTION_FAILED:
				publisher_event = "连接失败..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED:
				publisher_event = "连接成功..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_DISCONNECTED:
				publisher_event = "连接断开..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_STOP:
				publisher_event = "关闭..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RECORDER_START_NEW_FILE:
				publisher_event = "开始一个新的录像文件 : " + param3;
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_ONE_RECORDER_FILE_FINISHED:
				if (record_executor_ != null) {
					RecordExecutorService executor = record_executor_.get();
					if (executor != null) {
						RecordFileFinishedHandler file_finished_handler = new RecordFileFinishedHandler().set(handle, param3, param1);
						if (param2 > 0)
							file_finished_handler.set_begin_time(param2);

						executor.execute(file_finished_handler);
					}
				}
				publisher_event = "已生成一个录像文件 : " + param3;
				break;

			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_SEND_DELAY:
				publisher_event = "发送时延: " + param1 + " 帧数:" + param2;
				break;

			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_CAPTURE_IMAGE:
				publisher_event = "快照: " + param1 + " 路径:" + param3;
				if (0 == param1)
					publisher_event = publisher_event + "截取快照成功.." + ", 用户数据:" + param4;
				 else
					publisher_event = publisher_event + "截取快照失败..";

				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL:
				publisher_event = "RTSP服务URL: " + param3;
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUSH_RTSP_SERVER_RESPONSE_STATUS_CODE:
				publisher_event ="RTSP status code received, codeID: " + param1 + ", RTSP URL: " + param3;
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PUSH_RTSP_SERVER_NOT_SUPPORT:
				publisher_event ="服务器不支持RTSP推送, 推送的RTSP URL: " + param3;
				break;
		}

		String str = "当前回调状态:" + publisher_event;

		Log.i(TAG, str);

		if (handler_ != null) {
			android.os.Handler handler = handler_.get();
			if (handler != null) {
				Message message = new Message();
				message.what = PUBLISHER_EVENT_MSG;
				message.obj = publisher_event;
				handler.sendMessage(message);
			}
		}
	}

	public NTSmartEventCallbackV2 set(android.os.Handler handler, RecordExecutorService record_executor) {
		this.handler_ = new WeakReference<>(handler);
		this.record_executor_ = new WeakReference<>(record_executor);
		return this;
	}

	private WeakReference<android.os.Handler> handler_;
	private WeakReference<RecordExecutorService> record_executor_;
}

onDestroy() 的时候,调用stopPush()即可,如果有录像和快照,都停掉,此外,停掉图层投递线程,并关闭camera:

@Override
protected void onDestroy() {
	Log.i(TAG, "activity destory!");

	record_executor_.cancel_tasks();

	stopAudioRecorder();

	if (snap_shot_impl_ != null) {
		snap_shot_impl_.stop();
		snap_shot_impl_ = null;
	}

	snap_shot_publisher_.release();

	stopPush();
	stopRecorder();

	stream_publisher_.release();

	stopLayerPostThread();

	if (camera2Helper != null) {
		camera2Helper.release();
	}

	if (!record_executor_.shutdown(60, TimeUnit.SECONDS))
		Log.w(TAG, "call record_executor_.shutdown failed");

	super.onDestroy();
}

总结

以上是大牛直播SDK的Android平台RTMP直播推送模块详细的对接说明,除了可以对接编码前各种类型的音视频数据外,模块还支持对接编码后音视频数据,并实现本地录像、快照等功能,除支持H.264外,RTMP推送模块还支持扩展H.265和Enhanced RTMP。感兴趣的开发者,可以单独跟我们探讨。

Android平台外部编码数据实时预览播放SDK

Android平台除RTMP、RTSP直播播放外,有些场景可输出编码后(视频:H.264/H.265,音频:AAC/PCMA/PCMU)的数据,比如无人机或类似智能硬件设备,回调出来的H.264/H.265数据,编码后的H.264/H.265数据除了正常转推到RTMP、轻量级RTSP服务或GB28181外,还需要本地预览甚至对数据做二次处理(视频分析、实时水印字符叠加等),大概流程如下:

基于这样的场景诉求,我们开发了Android平台外部编码数据实时预览播放模块(以RTSP拉流,然后回调上来编码后数据,再投递到Android平台编码数据实时预览播放SDK为例)。

Android平台外部编码数据实时预览模块可实现本地低延迟的预览播放,支持软解码和特定机型硬解码,支持等比例或铺满显示,感兴趣的开发者可参看:Android平台如何实现第三方模块编码后(H.264/H.265/AAC/PCMA/PCMU)数据实时预览播放和播放

Android平台国网B接口接入SDK

电网视频监控系统是智能电网的一个重要组成部分,广泛应用于电网的建设、生产、运行、经营等方面。由于视频监控系统在不同的建设时期选用了不同的技术和不同厂家的产品,导致了标准不统一、技术路线不一致。目前国家电网公司智能电网建设,对视频监控系统提出了新的要求,因此实现统一监控、统一存储、分级控制、分域管理,使不同的视频监视系统能够互联互通,满足视频监控系统全局化、整体化的发展需求,已成为亟待解决的问题。

为此,大牛直播SDK推出的Android平台国网B接口接入SDK,可实现不具备国网B接口音视频能力的 Android终端,通过平台注册接入到现有的平台。

Android终端除支持常规的音视频数据接入外,还可以支持移动设备位置(MobilePosition)订阅和通知、语音广播,支持对接数据类型如下:

  1. 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型),其中,Android平台前后摄像头数据,或者屏幕数据,或者Unity拿到的数据,均属编码前数据;
  2. 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);
  3. 拉取RTSP或RTMP流并接入至GB28181平台(比如其他IPC的RTSP流,可通过Android平台GB28181接入到国标平台)。

技术特点和优势:

  1. 全自研框架,易于扩展,自适应算法让延迟更低、采集编码传输效率更高;
  2. 所有功能以SDK接口形式提供,支持状态反馈;
  3. 可同时运行RTMP直播推送SDK、轻量级RTSP服务SDK和录像SDK;
  4. 支持外部YUV/RGB/H.264/H.265/AAC数据源接入;
  5. 所有参数均可通过SDK接口单独设置,亦可通过默认参数,傻瓜式设置。

功能支持:

  •  [视频格式]H.264/H.265(Android H.265硬编码);
  •  [音频格式]G.711 A律、AAC;
  •  [音量调节]Android平台采集端支持实时音量调节;
  •  [H.264硬编码]支持H.264特定机型硬编码;
  •  [H.265硬编码]支持H.265特定机型硬编码;
  •  [软硬编码参数配置]支持gop间隔、帧率、bit-rate设置;
  •  [软编码参数配置]支持软编码profile、软编码速度、可变码率设置;
  •  支持横屏、竖屏推流;
  •  Android平台支持后台service推送屏幕(推送屏幕需要5.0+版本);
  • 支持RTP OVER UDP和RTP OVER TCP被动模式;
  • 支持信令通道网络传输协议TCP/UDP设置;
  • 支持注册、注销,支持注册刷新及注册有效期设置;
  • 支持前端资源上报(Push_Resourse);
  • 支持请求获取资源(Request_Resource)应答;
  • 支持移动设备位置(MobilePosition)订阅和通知;
  • 支持语音广播;
  •  [实时水印]支持动态文字水印、png水印;
  •  [镜像]Android平台支持前置摄像头实时镜像功能;
  •  [实时静音]支持实时静音/取消静音;
  •  [实时快照]支持实时快照;
  •  [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
  •  [外部编码前视频数据对接]支持YUV数据对接;
  •  [外部编码前音频数据对接]支持PCM对接;
  •  [外部编码后视频数据对接]支持外部H.264数据对接;
  •  [外部编码后音频数据对接]外部AAC数据对接;
  •  [扩展录像功能]支持和录像SDK组合使用,录像相关功能。

对应Demo:

  •  Android工程:SmartPublisherV2、Camera2Demo

Android平台GB28181接入SDK

大牛直播SDK推出的Android平台GB28181接入SDK(SmartGBD),可实现不具备国标音视频能力的 Android终端,通过平台注册接入到现有的GB/T28181—2016或GB/T28181—2022服务,可用于如执法记录仪、智能安全帽、智能监控、智慧零售、智慧教育、远程办公、明厨亮灶、智慧交通、智慧工地、雪亮工程、平安乡村、生产运输、车载终端等场景,可能是业内为数不多功能齐全性能优异的商业级水准GB28181接入SDK。

Android终端除支持常规的音视频数据接入外,还可以支持移动设备位置(MobilePosition)订阅和通知、图像抓拍、语音广播和语音对讲、历史视音频下载和回放,支持对接数据类型如下:

  1. 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型),其中,Android平台前后摄像头数据,或者屏幕数据,或者Unity拿到的数据,均属编码前数据;
  2. 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);
  3. 拉取RTSP或RTMP流并接入至GB28181平台(比如其他IPC的RTSP流,可通过Android平台GB28181接入到国标平台)。

技术特点和优势:

  1. 全自研框架,易于扩展,自适应算法让延迟更低、采集编码传输效率更高;
  2. 所有功能以SDK接口形式提供,支持状态反馈;
  3. 可同时运行RTMP直播推送SDK、轻量级RTSP服务SDK和录像SDK;
  4. 支持外部YUV/RGB/H.264/H.265/AAC数据源接入;
  5. 所有参数均可通过SDK接口单独设置,亦可通过默认参数,傻瓜式设置。

功能支持:

  •  [视频格式]H.264/H.265(Android H.265硬编码);
  •  [音频格式]G.711 A律、AAC;
  •  [音量调节]Android平台采集端支持实时音量调节;
  •  [H.264硬编码]支持H.264特定机型硬编码;
  •  [H.265硬编码]支持H.265特定机型硬编码;
  •  [软硬编码参数配置]支持gop间隔、帧率、bit-rate设置;
  •  [软编码参数配置]支持软编码profile、软编码速度、可变码率设置;
  •  支持横屏、竖屏推流;
  •  Android平台支持后台service推送屏幕(推送屏幕需要5.0+版本);
  • 支持纯视频、音视频PS打包传输;
  • 支持RTP OVER UDP和RTP OVER TCP被动模式(TCP媒体流传输客户端);
  • 支持信令通道网络传输协议TCP/UDP设置;
  • 支持注册、注销,支持注册刷新及注册有效期设置;
  • 支持设备目录查询应答;
  • 支持心跳机制,支持心跳间隔、心跳检测次数设置;
  • 支持移动设备位置(MobilePosition)订阅和通知;
  •  适用国家标准:GB/T 28181—2016、GB/T28181—2022;
  • 支持语音广播;
  • 支持语音对讲;
  • 支持图像抓拍;
  • 支持历史视音频文件检索;
  • 支持历史视音频文件下载;
  • 支持历史视音频文件回放;
  • 支持云台控制和预置位查询;
  •  [实时水印]支持动态文字水印、png水印;
  •  [镜像]Android平台支持前置摄像头实时镜像功能;
  •  [实时静音]支持实时静音/取消静音;
  •  [实时快照]支持实时快照;
  •  [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
  •  [外部编码前视频数据对接]支持YUV数据对接;
  •  [外部编码前音频数据对接]支持PCM对接;
  •  [外部编码后视频数据对接]支持外部H.264数据对接;
  •  [外部编码后音频数据对接]外部AAC数据对接;
  •  [扩展录像功能]支持和录像SDK组合使用,录像相关功能。

对应Demo:

  •  Android工程:SmartPublisherV2、Camera2Demo;

技术对接文档:

相关博客:

Unity环境下RTMP推流|轻量级RTSP服务+RTMP|RTSP播放低延迟解决方案

除了Windows/Linux/Android/iOS Native SDK,大牛直播SDK发布了Unity环境下的RTMP推流|轻量级RTSP服务(Windows平台+Linux平台+Android平台)和RTMP|RTSP直播播放(Windows、Linux、Android和iOS平台全覆盖)低延迟的解决方案。与Unity引擎深度融合、效能全开,全平台超低延迟,沉浸无界。

目前,大牛直播SDK的Unity3D环境下,已覆盖以下SDK:

  •  Windows平台RTMP直播推送SDK(采集Unity窗体、摄像头或屏幕);
  •  Windows平台轻量级RTSP服务SDK(采集Unity窗体、摄像头或屏幕);
  •  Windows平台RTMP|RTSP直播播放SDK;
  •  Linux平台RTMP直播推送SDK(采集Unity窗体、Unity声音);
  •  Linux平台RTMP|RTSP直播播放SDK;
  •  Android平台RTMP直播推送SDK(采集Unity窗体、摄像头、麦克风或Unity声音);
  •  Android平台轻量级RTSP服务SDK(采集Unity窗体、摄像头、麦克风或Unity声音);
  •  Android平台RTMP|RTSP直播播放SDK;
  •  iOS平台RTMP|RTSP直播播放SDK。

平台覆盖和架构支持

支持平台 支持架构
Windows平台 x86 debug/release, x64 debug/release
Linux(含麒麟操作系统) x86_64、aarch64
Android平台 armeabi-v7a, arm64-v8a, x86, x86_64
iOS平台 arm64

1. Unity环境下RTMP推流、轻量级RTSP服务模块

Unity环境下,不管是camera还是窗体数据也好,主要是高效率的拿到原始数据,采集端可用的数据格式是RGB的,拿到之后,通过高效率的数据传递,发给封装后的原生SDK,完成数据编码和RTMP推送。

需要注意的地方有几点:

1. 数据采集投递,确保高效率;

2. 屏幕分辨率发生变化,可实时适配;

3. Unity和原生SDK之间通信,比如event回调等;

4. 屏幕数据如有水平或垂直翻转,需要有一定的矫正。

2. Unity环境下RTMP|RTSP播放器

Unity 直播播放先锋,深耕传统行业赋能革新

早在数年前,大牛直播 SDK 便前瞻性地布局 Unity 环境下的 RTMP 和 RTSP 直播播放领域,以技术先行之姿,深度嵌入诸多传统行业关键脉络,成为驱动行业数字化转型、效能进阶的隐形引擎。

教育革新:知识跨时空,互动零延迟

在教育版图,打破地域与硬件束缚,将课堂装进 Unity 构建的虚拟空间。实时 RTMP 直播播放让名师课程穿越城市界限,偏远地区学子与名校课堂无缝对接。高清画面流畅呈现复杂实验演示、精美课件讲解,RTSP 技术加持下,校园监控实时回传,保障校园安全同时为远程教学管理提供一手资料。师生互动环节,借助超低延迟特性,提问瞬间达,解答零时差,虚拟教具操作同步反馈,无论是 K12 基础教育的趣味课堂,还是高校专业学科的深度研讨,都被重塑为沉浸式、互动型学习新生态。

工业仿真:虚拟预演,精准操控

工业制造与设计场景中,Unity 结合 RTMP/RTSP 直播播放大放异彩。新品研发阶段,异地团队通过直播实时观看高精度 3D 产品仿真模拟,机械运转细节、流体力学效果纤毫毕现,各方意见实时交流,加速迭代周期。生产线上,RTSP 实时回传设备运行状况,工程师远程监测参数、诊断故障,宛如亲临现场;复杂工业操控培训借 RTMP 低延迟实现远程模拟实操,新手在虚拟环境跟随导师动作即时反馈,精准掌握重型机械、精密仪器操作要诀,降低培训成本,提升实操安全系数。

低延迟控制:指令闪达,效能飞升

特殊作业与智能管控领域,毫秒级延迟决定成败。消防应急演练时,指挥中心通过 RTMP 直播精准掌控现场动态,下达疏散、救援指令瞬间抵达一线人员终端,人员位置、火势蔓延态势实时更新;电力巡检无人机搭载 RTSP 直播设备,飞巡线路画面即刻回传中控,隐患排查精准定位,远程操控指令同步执行,保障电网安全。智能仓储物流里,机械臂抓取、货物分拣动作经直播监控与控制指令零时差交互,提升自动化作业流畅度,减少货物损耗,以技术之力夯实行业高效运转根基,持续释放传统行业新潜能。

相关实现逻辑如下:

1. Native RTSP或RTSP直播播放SDK回调RGB/YUV420/NV12等其中的一种未压缩的图像格式;

2. Unity3D创建相应的RGB/YUV420等Shader;

3.Unity3D从各个平台获取图像数据来填充纹理即可;

需要注意的有几点:

1. 多实例支持:播放端和推送不一样,比如智慧城市,播放端有多路场景,所以多实例支持是必备功能,多实例环境下,需要能有好的区分event状态回调等;

2. 尽可能高效率的数据传递,确保资源占有最小化;

3. 视频分辨率变化后,能自动适配;

4. Unity和原生SDK之间通信,比如event回调等;

5. 长时间运行稳定性。

相关SDK文档及视频

大牛直播SDK Unity3D接口调用SDK说明

Unity3d RTSP/RTMP直播播放端SDK视频演示1

Unity3d RTSP/RTMP直播播放端SDK视频演示2

相关博客

Windows平台Unity3d下如何同时播放多路RTSP或RTMP流

如何在Unity3d平台下低延迟播放RTMP或RTSP流

Windows平台实现Unity下窗体|摄像头|屏幕采集推送

Android平台实现Unity3D下RTMP推送

Unity3D平台实现全景实时RTMP|RTSP流渲染

Unity3D下Linux平台播放RTSP或RTMP流

Android平台实现VR头显Unity下音视频数据RTMP推送

Unity实现Camera和Audio数据的低延迟RTMP推送技术探讨

Android平台Unity下如何通过WebCamTexture采集摄像头数据并推送至RTMP服务器或轻量级RTSP服务

Windows平台实现Unity下窗体|摄像头|屏幕采集推送

技术背景

随着Unity3D的应用范围越来越广,越来越多的行业开始基于Unity3D开发产品,如传统行业中虚拟仿真教育、航空工业、室内设计、城市规划、工业仿真等领域。

基于此,好多开发者苦于在Unity环境下,没有低延迟的推拉流解决方案,前几年,我们在Unity环境下推出了跨平台低延迟的RTMP|RTSP直播播放器,很好的解决了好多对延迟要求苛刻的使用场景。

随着时间的推移,越来越多的开发者联系我们,希望我们能推出Unity环境下的RTMP推送模块,获取到unity的实时数据,更低延迟更高效率的实现数据传输推送,基于此,我们发布了Unity环境下的RTMP推送模块。

本文以Windows平台为例,数据源分别为Unity的窗口、摄像头或整个屏幕,编码传输模块,还是调用大牛直播SDK(官方)的原生接口,简单界面先睹为快:

技术实现

1. 基础初始化


        private bool InitSDK()
        {
            if (!is_pusher_sdk_init_)
            {
                // 设置日志路径(请确保目录存在)
                String log_path = "D:\\pulisherlog";
                NTSmartLog.NT_SL_SetPath(log_path);

                UInt32 isInited = NTSmartPublisherSDK.NT_PB_Init(0, IntPtr.Zero);

                if (isInited != 0)
                {
                    Debug.Log("调用NT_PB_Init失败..");
                    return false;
                }

                is_pusher_sdk_init_ = true;
            }

            return true;
        }

2. 调用Open()接口,获取推送实例

       public bool OpenPublisherHandle(uint video_option, uint audio_option)
        {
            if (publisher_handle_ != IntPtr.Zero)
            {
                return true;
            }

            publisher_handle_count_ = 0;

            if (NTBaseCodeDefine.NT_ERC_OK != NTSmartPublisherSDK.NT_PB_Open(out publisher_handle_,
                video_option, audio_option, 0, IntPtr.Zero))
            {
                return false;
            }

            if (publisher_handle_ != IntPtr.Zero)
            {
                pb_event_call_back_ = new NT_PB_SDKEventCallBack(PbEventCallBack);

                NTSmartPublisherSDK.NT_PB_SetEventCallBack(publisher_handle_, IntPtr.Zero, pb_event_call_back_);

                return true;
            }
            else
            {
                return false;
            }
        }

3. 初始化参数配置

这里需要注意下,如果要采集unity窗口,需要设置图层模式,先填充一层RGBA黑色背景,然后再添加一层,用于叠加外部数据。

       private void SetCommonOptionToPublisherSDK()
        {
            if (!IsPublisherHandleAvailable())
            {
                Debug.Log("SetCommonOptionToPublisherSDK, publisher handle with null..");
                return;
            }

            NTSmartPublisherSDK.NT_PB_ClearLayersConfig(publisher_handle_, 0,
                            0, IntPtr.Zero);

            if (video_option == NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_LAYER)
            {
                // 第0层填充RGBA矩形, 目的是保证帧率, 颜色就填充全黑
                int red = 0;
                int green = 0;
                int blue = 0;
                int alpha = 255;

                NT_PB_RGBARectangleLayerConfig rgba_layer_c0 = new NT_PB_RGBARectangleLayerConfig();

                rgba_layer_c0.base_.type_ = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_RGBA_RECTANGLE;
                rgba_layer_c0.base_.index_ = 0;
                rgba_layer_c0.base_.enable_ = 1;
                rgba_layer_c0.base_.region_.x_ = 0;
                rgba_layer_c0.base_.region_.y_ = 0;
                rgba_layer_c0.base_.region_.width_ = video_width_;
                rgba_layer_c0.base_.region_.height_ = video_height_;

                rgba_layer_c0.base_.offset_ = Marshal.OffsetOf(rgba_layer_c0.GetType(), "base_").ToInt32();
                rgba_layer_c0.base_.cb_size_ = (uint)Marshal.SizeOf(rgba_layer_c0);

                rgba_layer_c0.red_ = System.BitConverter.GetBytes(red)[0];
                rgba_layer_c0.green_ = System.BitConverter.GetBytes(green)[0];
                rgba_layer_c0.blue_ = System.BitConverter.GetBytes(blue)[0];
                rgba_layer_c0.alpha_ = System.BitConverter.GetBytes(alpha)[0];

                IntPtr rgba_conf = Marshal.AllocHGlobal(Marshal.SizeOf(rgba_layer_c0));

                Marshal.StructureToPtr(rgba_layer_c0, rgba_conf, true);

                UInt32 rgba_r = NTSmartPublisherSDK.NT_PB_AddLayerConfig(publisher_handle_, 0,
                                rgba_conf, (int)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_RGBA_RECTANGLE,
                                0, IntPtr.Zero);

                Marshal.FreeHGlobal(rgba_conf);

                NT_PB_ExternalVideoFrameLayerConfig external_layer_c1 = new NT_PB_ExternalVideoFrameLayerConfig();

                external_layer_c1.base_.type_ = (Int32)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME;
                external_layer_c1.base_.index_ = 1;
                external_layer_c1.base_.enable_ = 1;
                external_layer_c1.base_.region_.x_ = 0;
                external_layer_c1.base_.region_.y_ = 0;
                external_layer_c1.base_.region_.width_ = video_width_;
                external_layer_c1.base_.region_.height_ = video_height_;

                external_layer_c1.base_.offset_ = Marshal.OffsetOf(external_layer_c1.GetType(), "base_").ToInt32();
                external_layer_c1.base_.cb_size_ = (uint)Marshal.SizeOf(external_layer_c1);

                IntPtr external_layer_conf = Marshal.AllocHGlobal(Marshal.SizeOf(external_layer_c1));

                Marshal.StructureToPtr(external_layer_c1, external_layer_conf, true);

                UInt32 external_r = NTSmartPublisherSDK.NT_PB_AddLayerConfig(publisher_handle_, 0,
                                external_layer_conf, (int)NTSmartPublisherDefine.NT_PB_E_LAYER_TYPE.NT_PB_E_LAYER_TYPE_EXTERNAL_VIDEO_FRAME,
                                0, IntPtr.Zero);

                Marshal.FreeHGlobal(external_layer_conf);

            }
            else if (video_option == NTSmartPublisherDefine.NT_PB_E_VIDEO_OPTION.NT_PB_E_VIDEO_OPTION_CAMERA)
            {
                CameraInfo camera = cameras_[cur_sel_camera_index_];
                NT_PB_VideoCaptureCapability cap = camera.capabilities_[cur_sel_camera_resolutions_index_];

                SetVideoCaptureDeviceBaseParameter(camera.id_.ToString(), (UInt32)cap.width_, (UInt32)cap.height_);
            }

            SetFrameRate((UInt32)CalBitRate(edit_key_frame_, video_width_, video_height_));

            Int32 type = 0;   //软编码
            Int32 encoder_id = 1;
            UInt32 codec_id = (UInt32)NTCommonMediaDefine.NT_MEDIA_CODEC_ID.NT_MEDIA_CODEC_ID_H264;
            Int32 param1 = 0;

            SetVideoEncoder(type, encoder_id, codec_id, param1);

            SetVideoQualityV2(CalVideoQuality(video_width_, video_height_, is_h264_encoder));

            SetVideoMaxBitRate((CalMaxKBitRate(edit_key_frame_, video_width_, video_height_, false)));

            SetVideoKeyFrameInterval((edit_key_frame_));

            if (is_h264_encoder)
            {
                SetVideoEncoderProfile(1);
            }

            SetVideoEncoderSpeed(CalVideoEncoderSpeed(video_width_, video_height_, is_h264_encoder));

            // 音频相关设置

            SetAuidoInputDeviceId(0);
            SetPublisherAudioCodecType(1);
            SetPublisherMute(is_mute);
            SetEchoCancellation(0, 0);
            SetNoiseSuppression(0);
            SetAGC(0);
            SetVAD(0);
            SetInputAudioVolume(Convert.ToSingle(edit_audio_input_volume_));
        }

4. 数据采集

摄像头和屏幕的数据采集,还是调用原生的SDK接口,本文不再赘述,如果需要采集Unity窗体的数据,可以用参考以下代码:

        if ( texture_ == null || video_width_ != Screen.width || video_height_ != Screen.height)
        {
            Debug.Log("OnPostRender screen changed++ scr_width: " + Screen.width + " scr_height: " + Screen.height);

            if (screen_image_ != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(screen_image_);
                screen_image_ = IntPtr.Zero;
            }

            if (texture_ !=  null)
            {
                UnityEngine.Object.Destroy(texture_);
                texture_ = null;
            }

            video_width_ = Screen.width;
            video_height_ = Screen.height;

            texture_ = new Texture2D(video_width_, video_height_, TextureFormat.BGRA32, false);

            screen_image_ = Marshal.AllocHGlobal(video_width_ * 4 * video_height_);

            Debug.Log("OnPostRender screen changed--");

            return;
        }

        texture_.ReadPixels(new Rect(0, 0, video_width_, video_height_), 0, 0, false);
        texture_.Apply();

从 texture里面,通过调用 GetRawTextureData(),获取到原始数据。

5. 数据对接

数据对接,通过调用以下接口:

       public void OnPostRGBAData(IntPtr data, int length, int stride, int width, int height)
        {
            NT_PB_Image pb_image = new NT_PB_Image();
            
            pb_image.format_ = (int)NTSmartPublisherDefine.NT_PB_E_IMAGE_FORMAT.NT_PB_E_IMAGE_FORMAT_RGB32;
            pb_image.width_ = width;
            pb_image.height_ = height;
            pb_image.timestamp_ = 0;
            pb_image.cb_size_ = (UInt32)Marshal.SizeOf(pb_image);   

            pb_image.stride_ = new Int32[16];
            pb_image.stride_[0] = stride;

            pb_image.plane_size_ = new Int32[16];
            pb_image.plane_size_[0] = pb_image.stride_[0] * pb_image.height_;         

            pb_image.plane_ = new IntPtr[16];
            pb_image.plane_[0] = data;

            IntPtr image_data = Marshal.AllocHGlobal(Marshal.SizeOf(pb_image));

            Marshal.StructureToPtr(pb_image, image_data, true);

            NTSmartPublisherSDK.NT_PB_PostLayerImage(publisher_handle_, 0,
                            1, image_data, 0, IntPtr.Zero);

            Marshal.FreeHGlobal(image_data);
        }

6. 本地数据预览

        public bool StartPreview()
        {
            if(CheckPublisherHandleAvailable() == false)
                return false;

            video_preview_image_callback_ = new NT_PB_SDKVideoPreviewImageCallBack(SDKVideoPreviewImageCallBack);

            NTSmartPublisherSDK.NT_PB_SetVideoPreviewImageCallBack(publisher_handle_, (int)NTSmartPublisherDefine.NT_PB_E_IMAGE_FORMAT.NT_PB_E_IMAGE_FORMAT_RGB32, IntPtr.Zero, video_preview_image_callback_);

            if (NTBaseCodeDefine.NT_ERC_OK != NTSmartPublisherSDK.NT_PB_StartPreview(publisher_handle_, 0, IntPtr.Zero))
            {
                if (0 == publisher_handle_count_)
                {
                    NTSmartPublisherSDK.NT_PB_Close(publisher_handle_);
                    publisher_handle_ = IntPtr.Zero;
                }

                return false;
            }

            publisher_handle_count_++;

            is_previewing_ = true;

            return true;
        }

设置preview后,处理preview的数据回调

        //预览数据回调
        public void SDKVideoPreviewImageCallBack(IntPtr handle, IntPtr user_data, IntPtr image)
        {
            NT_PB_Image pb_image = (NT_PB_Image)Marshal.PtrToStructure(image, typeof(NT_PB_Image));

            NT_VideoFrame pVideoFrame = new NT_VideoFrame();

            pVideoFrame.width_ = pb_image.width_;
            pVideoFrame.height_ = pb_image.height_;

            pVideoFrame.stride_ = pb_image.stride_[0];

            Int32 argb_size = pb_image.stride_[0] * pb_image.height_;

            pVideoFrame.plane_data_ = new byte[argb_size];
            
            if (argb_size > 0)
            {
                Marshal.Copy(pb_image.plane_[0],pVideoFrame.plane_data_,0, argb_size);
            }

            {
                cur_image_ = pVideoFrame;
            }
        }      

7. 相关event回调处理

        private void PbEventCallBack(IntPtr handle, IntPtr user_data, 
            UInt32 event_id,
            Int64 param1,
            Int64 param2,
            UInt64 param3,
            UInt64 param4,
            [MarshalAs(UnmanagedType.LPStr)] String param5,
            [MarshalAs(UnmanagedType.LPStr)] String param6,
            IntPtr param7)
        {
            String event_log = "";

            switch (event_id)
            {
                case (uint)NTSmartPublisherDefine.NT_PB_E_EVENT_ID.NT_PB_E_EVENT_ID_CONNECTING:
                    event_log = "连接中";
                    if (!String.IsNullOrEmpty(param5))
                    {
                        event_log = event_log + " url:" + param5;
                    }
                    break;

                case (uint)NTSmartPublisherDefine.NT_PB_E_EVENT_ID.NT_PB_E_EVENT_ID_CONNECTION_FAILED:
                    event_log = "连接失败";
                    if (!String.IsNullOrEmpty(param5))
                    {
                        event_log = event_log + " url:" + param5;
                    }
                    break;

                case (uint)NTSmartPublisherDefine.NT_PB_E_EVENT_ID.NT_PB_E_EVENT_ID_CONNECTED:
                    event_log = "已连接";
                    if (!String.IsNullOrEmpty(param5))
                    {
                        event_log = event_log + " url:" + param5;
                    }
                    break;

                case (uint)NTSmartPublisherDefine.NT_PB_E_EVENT_ID.NT_PB_E_EVENT_ID_DISCONNECTED:
                    event_log = "断开连接";
                    if (!String.IsNullOrEmpty(param5))
                    {
                        event_log = event_log + " url:" + param5;
                    }
                    break;

                default:
                    break;
            }

            if(OnLogEventMsg != null) OnLogEventMsg.Invoke(event_id, event_log);
        }

8. 开始推送、停止推送

       public bool StartPublisher(String url)
        {
            if (CheckPublisherHandleAvailable() == false) return false;

            if (publisher_handle_ == IntPtr.Zero)
            {
                return false;
            }
            if (!String.IsNullOrEmpty(url))
            {
                NTSmartPublisherSDK.NT_PB_SetURL(publisher_handle_, url, IntPtr.Zero);
            }

            if (NTBaseCodeDefine.NT_ERC_OK != NTSmartPublisherSDK.NT_PB_StartPublisher(publisher_handle_, IntPtr.Zero))
            {
                if (0 == publisher_handle_count_)
                {
                    NTSmartPublisherSDK.NT_PB_Close(publisher_handle_);
                    publisher_handle_ = IntPtr.Zero;
                }

                is_publishing_ = false;

                return false;
            }

            publisher_handle_count_++;

            is_publishing_ = true;

            return true;
        }

        public void StopPublisher()
        {
            if (is_publishing_ == false) return;

            publisher_handle_count_--;
            NTSmartPublisherSDK.NT_PB_StopPublisher(publisher_handle_);

            if (0 == publisher_handle_count_)
            {
                NTSmartPublisherSDK.NT_PB_Close(publisher_handle_);
                publisher_handle_ = IntPtr.Zero;
            }

            is_publishing_ = false;
        }

9. 关闭实例

        public void Close()
        {
            if (0 == publisher_handle_count_)
            {
                NTSmartPublisherSDK.NT_PB_Close(publisher_handle_);
                publisher_handle_ = IntPtr.Zero;
            }
        }

总结

经测试,Unity环境下,通过高效率的数据采集、编码和推送,配合SmartPlayer播放器播放,整体延迟可控制在毫秒级,可适用于大多数Unity环境下对延迟和稳定性要求苛刻的场景。

Unity3D RTMP直播推流SDK

好多开发者苦于很难在unity3d下实现RTMP直播推送,本次以大牛直播SDK的Windows平台RTMP推送模块(以推摄像头为例,如需推屏幕数据,设置相关参数即可)为例,介绍下unity3d的RTMP推送集成。

简单来说,Unity3D环境下,可以直接调用C#的接口封装,针对此,我们先做了一层封装 (nt_publisher_wrapper.cs),核心代码如下:

初始化和基础参数设置:

       private bool InitSDK()
        {
            if (!is_pusher_sdk_init_)
            {
                // 设置日志路径(请确保目录存在)
                String log_path = "D:\\pulisherlog";
                NTSmartLog.NT_SL_SetPath(log_path);

                UInt32 isInited = NTSmartPublisherSDK.NT_PB_Init(0, IntPtr.Zero);

                if (isInited != 0)
                {
                    Debug.Log("调用NT_PB_Init失败..");
                    return false;
                }

                is_pusher_sdk_init_ = true;
            }

            return true;
        }

        public bool OpenPublisherHandle(uint video_option, uint audio_option)
        {
            if (publisher_handle_ != IntPtr.Zero)
            {
                return true;
            }

            publisher_handle_count_ = 0;

            if (NTBaseCodeDefine.NT_ERC_OK != NTSmartPublisherSDK.NT_PB_Open(out publisher_handle_,
                video_option, audio_option, 0, IntPtr.Zero))
            {
                return false;
            }

            if (publisher_handle_ != IntPtr.Zero)
            {
                pb_event_call_back_ = new NT_PB_SDKEventCallBack(PbEventCallBack);

                NTSmartPublisherSDK.NT_PB_SetEventCallBack(publisher_handle_, IntPtr.Zero, pb_event_call_back_);

                return true;
            }
            else
            {
                return false;
            }
        }

        private void SetCommonOptionToPublisherSDK()
        {
            if (!IsPublisherHandleAvailable())
            {
                Debug.Log("SetCommonOptionToPublisherSDK, publisher handle with null..");
                return;
            }

            CameraInfo camera = cameras_[cur_sel_camera_index_];
            NT_PB_VideoCaptureCapability cap = camera.capabilities_[cur_sel_camera_resolutions_index_];

            SetVideoCaptureDeviceBaseParameter(camera.id_.ToString(), (UInt32)cap.width_, (UInt32)cap.height_);

            SetFrameRate((UInt32)CalBitRate(edit_key_frame_, cap.width_, cap.height_));

            SetVideoEncoderType(is_h264_encoder ? 1 : 2);

            SetVideoQualityV2(CalVideoQuality(cap.width_, cap.height_, is_h264_encoder));

            SetVideoMaxBitRate((CalMaxKBitRate(edit_key_frame_, cap.width_, cap.height_, false)));

            SetVideoKeyFrameInterval((edit_key_frame_));

            if (is_h264_encoder)
            {
                SetVideoEncoderProfile(1);

            }

            SetVideoEncoderSpeed(CalVideoEncoderSpeed(cap.width_, cap.height_, is_h264_encoder));

            // 音频相关设置

            SetAuidoInputDeviceId(0);

            SetPublisherAudioCodecType(1);

            SetPublisherMute(is_mute);

            SetInputAudioVolume(Convert.ToSingle(edit_audio_input_volume_));
        }

预览、停止预览:

       public bool StartPreview()
        {
            if(CheckPublisherHandleAvailable() == false)
                return false;

            video_preview_image_callback_ = new NT_PB_SDKVideoPreviewImageCallBack(SDKVideoPreviewImageCallBack);

            NTSmartPublisherSDK.NT_PB_SetVideoPreviewImageCallBack(publisher_handle_, (int)NTSmartPublisherDefine.NT_PB_E_IMAGE_FORMAT.NT_PB_E_IMAGE_FORMAT_RGB32, IntPtr.Zero, video_preview_image_callback_);

            if (NTBaseCodeDefine.NT_ERC_OK != NTSmartPublisherSDK.NT_PB_StartPreview(publisher_handle_, 0, IntPtr.Zero))
            {
                if (0 == publisher_handle_count_)
                {
                    NTSmartPublisherSDK.NT_PB_Close(publisher_handle_);
                    publisher_handle_ = IntPtr.Zero;
                }

                return false;
            }

            publisher_handle_count_++;

            is_previewing_ = true;

            return true;
        }

        public void StopPreview()
        {
            if (is_previewing_ == false) return;

            is_previewing_ = false;

            publisher_handle_count_--;
            NTSmartPublisherSDK.NT_PB_StopPreview(publisher_handle_);

            if (0 == publisher_handle_count_)
            {
                NTSmartPublisherSDK.NT_PB_Close(publisher_handle_);
                publisher_handle_ = IntPtr.Zero;
            }
        }

开始推送、停止推送:

        public bool StartPublisher(String url)
        {
            if (CheckPublisherHandleAvailable() == false) return false;

            if (publisher_handle_ == IntPtr.Zero)
            {
                return false;
            }
            if (!String.IsNullOrEmpty(url))
            {
                NTSmartPublisherSDK.NT_PB_SetURL(publisher_handle_, url, IntPtr.Zero);
            }

            if (NTBaseCodeDefine.NT_ERC_OK != NTSmartPublisherSDK.NT_PB_StartPublisher(publisher_handle_, IntPtr.Zero))
            {
                if (0 == publisher_handle_count_)
                {
                    NTSmartPublisherSDK.NT_PB_Close(publisher_handle_);
                    publisher_handle_ = IntPtr.Zero;
                }

                is_publishing_ = false;

                return false;
            }

            publisher_handle_count_++;

            is_publishing_ = true;

            return true;
        }

        public void StopPublisher()
        {
            if (is_publishing_ == false) return;

            publisher_handle_count_--;
            NTSmartPublisherSDK.NT_PB_StopPublisher(publisher_handle_);

            if (0 == publisher_handle_count_)
            {
                NTSmartPublisherSDK.NT_PB_Close(publisher_handle_);
                publisher_handle_ = IntPtr.Zero;
            }

            is_publishing_ = false;
        }

相关event事件回调:

        private void PbEventCallBack(IntPtr handle, IntPtr user_data, 
            UInt32 event_id,
            Int64 param1,
            Int64 param2,
            UInt64 param3,
            UInt64 param4,
            [MarshalAs(UnmanagedType.LPStr)] String param5,
            [MarshalAs(UnmanagedType.LPStr)] String param6,
            IntPtr param7)
        {
            String event_log = "";

            switch (event_id)
            {
                case (uint)NTSmartPublisherDefine.NT_PB_E_EVENT_ID.NT_PB_E_EVENT_ID_CONNECTING:
                    event_log = "连接中";
                    if (!String.IsNullOrEmpty(param5))
                    {
                        event_log = event_log + " url:" + param5;
                    }
                    break;

                case (uint)NTSmartPublisherDefine.NT_PB_E_EVENT_ID.NT_PB_E_EVENT_ID_CONNECTION_FAILED:
                    event_log = "连接失败";
                    if (!String.IsNullOrEmpty(param5))
                    {
                        event_log = event_log + " url:" + param5;
                    }
                    break;

                case (uint)NTSmartPublisherDefine.NT_PB_E_EVENT_ID.NT_PB_E_EVENT_ID_CONNECTED:
                    event_log = "已连接";
                    if (!String.IsNullOrEmpty(param5))
                    {
                        event_log = event_log + " url:" + param5;
                    }
                    break;

                case (uint)NTSmartPublisherDefine.NT_PB_E_EVENT_ID.NT_PB_E_EVENT_ID_DISCONNECTED:
                    event_log = "断开连接";
                    if (!String.IsNullOrEmpty(param5))
                    {
                        event_log = event_log + " url:" + param5;
                    }
                    break;

                default:
                    break;
            }

            if(OnLogEventMsg != null) OnLogEventMsg.Invoke(event_id, event_log);
        }

SmartPublishWinMono.cs 调用上述封装的代码即可,本地预览的话,拿到回调的RGB数据,在unity3d上层刷下即可,如下图:

经测试,unity3d下,RTMP推送,配合RTMP播放端,依然可以实现毫秒级延迟的推拉流体验。

大牛直播SDK试用、测试服务协议

欢迎使用上海视沃信息科技有限公司(以下简称“视沃科技”)旗下“大牛直播SDK”,试用测试前,请您仔细阅读视沃科技官方网站公布的相关规范和使用流程及本协议的全部内容,如您不同意前述任意内容,请不要进行后续操作。如您实际使用了“大牛直播SDK”,我方将视为您已完全理解并认同规范、流程及服务协议的全部内容。

  • 协议主体

本服务协议是因您使用大牛直播SDK与视沃科技所订立的有效合约。

  • 协议的订立与生效

一旦您选择试用、测试大牛直播SDK并进行后续操作,即表示您同意遵循本服务协议之所有约定,本协议即成为双方之间就大牛直播SDK软件包服务达成的有效合约。

  • 大牛直播SDK软件包服务的使用

3.1 在试用测试大牛直播SDK软件包服务前,您应知悉阅读视沃科技官网页面上的相关规范、使用流程,并理解相关内容及可能发生的后果,在使用大牛直播SDK软件包服务的过程中,您应依照相关操作指引进行操作,请您自行把握风险谨慎操作。

3.2您理解并同意,使用大牛直播SDK软件服务包是您自行独立审慎判断的结果,您将自行对此负责,包括但不限于:

3.2.1在使用过程中,您将对自行操作的行为及产生的结果负责;

3.2.1在试用、测试大牛直播SDK软件服务包阶段可免费使用,但正式授权版需签订购买合同并支付相关费用;

3.2.3在使用大牛直播SDK软件包过程中,您不应进行任何破坏或试图破坏网络安全的行为,您承诺SDK试用、测试及授权后的使用场景保持一致且视频内容合法合规,不得含有我国法律、行政法规禁止发布或传输的信息,为履行法律赋予的安全管理义务,请您下载填写附件1并将签章后的附件1发至指定邮箱;

3.2.4除视沃科技明示许可外,不得修改、翻译、改编、转许可、转让大牛直播SDK软件服务包,也不得逆向工程、反编译或试图以其他方式发现大牛直播SDK软件服务包或软件源代码;

3.2.5您不应以任何将违反国家、地方法律法规、行业管理和社会公共道德、及影响、损害或可能影响、损害视沃科技利益的方式或目的使用大牛直播SDK。

  • 责任限制

您理解并同意,在免费测试、试用其间,视沃科技虽然对大牛直播SDK软件包服务提供可用性支撑,但不对其中任何错误或漏洞提供任何担保,并不对您使用大牛直播SDK软件服务包的工作和结果承担任何责任。

  • 变更和终止

5.1 您理解并认可,视沃科技保留随时修改、取消、增强大牛直播SDK软件包服务一项或多项功能的权利;

5.2如您有任何违反本服务协议的情形,或根据视沃科技自己的独立判断认为您对大牛直播SDK的使用行为不符合我司要求,我司有权随时中断您使用大牛直播SDK而无需通知您,并将相关情况向有关主管部门报告;同时,如给我司造成损失,我司有权要求赔偿。

  • 保密

您及视沃科技都应对对方的保密信息承担保密责任,除非经国家行政、司法等有权机关要求披露或该信息已进入公有领域。

  • 其他

7.1视沃科技有权随时根据有关法律、法规的变化及公司经营状况和经营策略的调整等修改本服务协议。修改后的服务协议会在视沃科技官网公布。如果不同意修改的内容,您应停止使用大牛直播SDK软件包服务。如果继续使用大牛直播SDK软件包,则视为您接受本服务协议的变动。

7.2如果本服务协议中的任何条款无论因何种原因完全或部分无效或不具有执行力,或违反任何适用的法律,则该条款被视为删除,但本服务协议的其余条款仍应有效并且有约束力。

7.3本服务协议受中华人民共和国法律管辖。在执行本服务协议过程中如发生纠纷,双方应及时协商解决。协商不成时,任何一方可直接向上海市长宁区人民法院提起诉讼。

附件1:

  • 甲方(测试、试用方公司名称):
  • 测试、试用大牛直播SDK实际使用场景描述:(附文字说明、产品截图、视频画面)
  • 在使用大牛直播SDK软件包过程中,您不应进行任何破坏或试图破坏网络安全的行为,您承诺SDK试用、测试及授权后的使用场景保持一致且视频内容合法合规,不得含有我国法律、行政法规禁止发布或传输的信息

附件1请发送至 1130758427@qq.com

 

甲方签章

xxxx年xx月xx日

 

视沃科技(大牛直播SDK)官方测试版获取流程

视沃科技官方测试版本暂不提供网络下载,如因产品需求,需要测试版,可按照以下流程:

  1. 联系视沃科技官方商务|技术人员电话、QQ或微信,手机:130-7210-2209 或 135-6452-9354  QQ:89030985  或 517631076 微信:xinsheng120 或 ldxevt
  2. 查看“大牛直播SDK试用、测试服务协议.pdf”;右键另存为
  3. 填写“上海视沃信息科技有限公司SDK使用场景调查表”(右键另存为),并签章(主要是写清楚使用的行业,和需要试用的模块,场景调查表仅作为试用企业场景合法合规审核留底,不作为其他任何用途,也不产生任何费用,如果公司/学校签章流程复杂,您也可以填写后,打印出来自己签字+留联系方式+工牌(或可证明隶属于本公司的材料即可);
  4. 公司审核通过后,获取试用版和前期技术支持。

Windows平台RTSP|RTMP播放端SDK集成说明

2.1 demo说明

  • 大牛直播SDK提供C++/C#两套接口,对外提供32/64位debug/release库,C++和C#接口一一对应,C#接口比C++接口增加前缀NT_PB_;
  • WIN-PlayerSDK-CPP-Demo:播放端SDK对应的C++接口的demo;
  • WIN-PlayerSDK-CSharp-Demo:播放端SDK对应的C#接口的demo;
  • 播放端SDK支持Win7及以上系统;
  • 本demo基于VS2013开发。

2.2 界面UI展示

2.3集成说明

C++头文件:

  • [类型定义]nt_type_define.h
  • [Log定义]smart_log.h
  • [Log定义]smart_log_define.h
  • [base code定义]nt_base_code_define.h
  • [player接口]smart_player_define.h
  • [player参数定义]smart_player_sdk.h

C#头文件:

  • [base code定义]nt_base_code_define.cs
  • [player接口]smart_player_define.cs
  • [player参数定义]smart_player_sdk.cs

相关Lib:

  • SmartLog.dll
  • SmartLog.lib
  • SmartPlayerSDK.dll
  • SmartPlayerSDK.lib
  • avcodec-56.dll
  • avdevice-56.dll
  • avfilter-5.dll
  • avformat-56.dll
  • avutil-54.dll
  • postproc-53.dll
  • swresample-1.dll
  • swscale-3.dll

集成步骤:

  1. 把lib目录下debug/release库拷贝到需要集成的工程对应的debug或release目录下(确保32位/64位库debug/release目录一一对应);

lib目录如下:

    1. 32位debug库:debug
    2. 32位release库:release
    3. 64位debug库:x64\debug
    4. 64位release库:x64\release

2. 相关cs头文件,加入需要集成的工程;

3. 在需要集成的工程,右键->Properties->

Application->Assembly name,大牛直播SDK按照APP名称授权,未授权版本,此处请改成“SmartPlayer”,如需授权,可直接联系商务;

4. 正式授权版,需要在Init()接口调用之前添加设置license的代码(相关Key和CID请根据正式授权版邮件说明填写):

2.4 接口调用时序(以C#为例)

2.4.1 设置授权license

C#的SDK,请在在NT.NTSmartPlayerSDK.NT_SP_Init之前添加下面的代码:

NT.NTSmartPlayerSDK.NT_SP_SetSDKClientKey("xxxxxxxxxx", "xxxxxxxxxx", 0, IntPtr.Zero);

UInt32 isInited = NT.NTSmartPlayerSDK.NT_SP_Init(0, IntPtr.Zero);
if (isInited != 0)
{
    MessageBox.Show("调用NT_SP_Init失败..");
    return;
}

C++的SDK,请在player_api_.Init之前添加下面的代码:

NT_SP_SetSDKClientKey(NT_SP_SetSDKClientKey("xxxxxxxxxx", "xxxxxxxxxx", 0, nullptr);

if ( NT_ERC_OK != player_api_.Init(0, NULL) )
{
    return FALSE;
}

2.4.2 设置日志存放路径

需要在player_api_.Init之前添加下面的代码:

// 设置日志路径(请确保目录存在)
String log_path = "D:\\playerlog";
NTSmartLog.NT_SL_SetPath(log_path);

如目录存在,并具备文件写入权限,关闭应用程序后,相关文件夹下会有smart_sdk.log生成。

2.4.3 初始化SDK

NT_SP_Init:SDK初始化,多实例播放,此接口仅需调用一次即可。

2.4.4 特定机型硬解码检测

如系统用于特定机型环境下,特别是多路播放场景,需用到硬解码的话,可以用以下两组接口检测系统是否支持硬解。

注:在软解性能满足系统需求的前提下,一般建议优先使用软解。

/*
 * 检查是否支持H264硬解码
 * 如果支持的话返回NT_ERC_OK
 */
[DllImport(@"SmartPlayerSDK.dll")]
public static extern UInt32 NT_SP_IsSupportH264HardwareDecoder();

/*
  * 检查是否支持H265硬解码
  * 如果支持的话返回NT_ERC_OK
  */
[DllImport(@"SmartPlayerSDK.dll")]
public static extern UInt32 NT_SP_IsSupportH265HardwareDecoder();

如需使用硬解码,调用如下接口即可:

NTSmartPlayerSDK.NT_SP_SetH264HardwareDecoder(player_handle_, is_support_h264_hardware_decoder_ ? 1 : 0, 0);
NTSmartPlayerSDK.NT_SP_SetH265HardwareDecoder(player_handle_, is_support_h265_hardware_decoder_ ? 1 : 0, 0);

2.4.5 Open生成播放实例

NT_SP_Open:每调用一次Open接口,对应一个播放实例,如需播放多实例,对应多个player handler。

if (player_handle_ == IntPtr.Zero)
{
    player_handle_ = new IntPtr();

    UInt32 ret_open = NTSmartPlayerSDK.NT_SP_Open(out player_handle_, IntPtr.Zero, 0, IntPtr.Zero);

    if (ret_open != 0)
    {
        player_handle_ = IntPtr.Zero;
        MessageBox.Show("调用NT_SP_Open失败..");
        return;
    }
}

2.4.6 设置回调事件

  1. NT_SP_SetEventCallBack:用于回调网络链接状态、buffer状态(开始、buffer比例、结束)、实时带宽等,对应EventID如下:
/*事件ID*/
public enum NT_SP_E_EVENT_ID : uint
{
        NT_SP_E_EVENT_ID_BASE = NTBaseCodeDefine.NT_EVENT_ID_SMART_PLAYER_SDK,

        NT_SP_E_EVENT_ID_CONNECTING          = NT_SP_E_EVENT_ID_BASE | 0x2, /*连接中*/
        NT_SP_E_EVENT_ID_CONNECTION_FAILED = NT_SP_E_EVENT_ID_BASE | 0x3, /*连接失败*/
        NT_SP_E_EVENT_ID_CONNECTED       = NT_SP_E_EVENT_ID_BASE | 0x4, /*已连接*/
        NT_SP_E_EVENT_ID_DISCONNECTED     = NT_SP_E_EVENT_ID_BASE | 0x5, /*断开连接*/
        NT_SP_E_EVENT_ID_NO_MEDIADATA_RECEIVED = NT_SP_E_EVENT_ID_BASE | 0x8,  /*收不到RTMP数据*/
        NT_SP_E_EVENT_ID_RTSP_STATUS_CODE   = NT_SP_E_EVENT_ID_BASE | 0xB,  /*rtsp status code上报, 目前只上报401, param1表示status code*/

        /* 接下来请从0x81开始*/
        NT_SP_E_EVENT_ID_START_BUFFERING = NT_SP_E_EVENT_ID_BASE | 0x81, /*开始缓冲*/
        NT_SP_E_EVENT_ID_BUFFERING     = NT_SP_E_EVENT_ID_BASE | 0x82, /*缓冲中, param1 表示百分比进度*/
        NT_SP_E_EVENT_ID_STOP_BUFFERING  = NT_SP_E_EVENT_ID_BASE | 0x83, /*停止缓冲*/

        NT_SP_E_EVENT_ID_DOWNLOAD_SPEED  = NT_SP_E_EVENT_ID_BASE | 0x91, /*下载速度, param1表示下载速度,单位是(Byte/s)*/

        NT_SP_E_EVENT_ID_PLAYBACK_REACH_EOS = NT_SP_E_EVENT_ID_BASE | 0xa1,     /*播放结束, 直播流没有这个事件,点播流才有*/
        NT_SP_E_EVENT_ID_RECORDER_REACH_EOS = NT_SP_E_EVENT_ID_BASE | 0xa2,     /*录像结束, 直播流没有这个事件, 点播流才有*/
        NT_SP_E_EVENT_ID_PULLSTREAM_REACH_EOS = NT_SP_E_EVENT_ID_BASE | 0xa3,   /*拉流结束, 直播流没有这个事件,点播流才有*/

        NT_SP_E_EVENT_ID_DURATION = NT_SP_E_EVENT_ID_BASE | 0xa8, /*视频时长,如果是直播,则不上报,如果是点播的话, 若能从视频源获取视频时长的话,则上报, param1表示视频时长,单位是毫秒(ms)*/
}

  1. NT_SP_SetVideoSizeCallBack:设置视频分辨率回调,如流数据携带视频数据,SDK会回上来视频宽高信息:
//video resolution callback
video_size_call_back_ = new SP_SDKVideoSizeCallBack(SP_SDKVideoSizeHandle);
NTSmartPlayerSDK.NT_SP_SetVideoSizeCallBack(player_handle_, IntPtr.Zero, video_size_call_back_);

注意:视频宽高回上来或绘制窗口发生变化时,记得调用NT_SP_OnWindowSize()更新,如不调用可能会引起视频模糊。

private void PlaybackWindowResized(Int32 width,Int32 height)
{
    width_=width;
    height_=height;

    int left=playWnd.Left;
    int top=playWnd.Top;

    textBox_resolution.Text=width+"*"+height;

    if(player_handle_==IntPtr.Zero)
    {
       return;
    }

    NTSmartPlayerSDK.NT_SP_OnWindowSize(player_handle_,playWnd.Width,playWnd.Height);
}
  1. NT_SP_SetVideoFrameCallBack:设置YUV/RGB32数据回调,可用于对接第三方视频分析,或自行绘制等,如系统不支持D3D绘制,可设置回调数据,上层GDI模式绘制:
/*定义视频帧图像格式*/
public enum NT_SP_E_VIDEO_FRAME_FORMAT : uint
{
      NT_SP_E_VIDEO_FRAME_FORMAT_RGB32 = 1, // 32位的rgb格式, r, g, b各占8, 另外一个字节保留, 内存字节格式为: bb gg rr xx, 主要是和windows位图匹配, 在小端模式下,按DWORD类型操作,最高位是xx, 依次是rr, gg, bb
      NT_SP_E_VIDEO_FRAME_FORMAT_ARGB = 2, // 32位的argb格式,内存字节格式是: bb gg rr aa 这种类型,和windows位图匹配
      NT_SP_E_VIDEO_FRAME_FROMAT_I420 = 3, // YUV420格式, 三个分量保存在三个面上
}

  1. NT_SP_SetVideoFrameCallBackV2:设置YUV/RGB32数据回调,与NT_SP_SetVideoFrameCallBack接口的不同在于,吐出来的视频数据, 可以指定宽高;
  2. NT_SP_SetRenderVideoFrameTimestampCallBack:设置绘制视频帧时,视频帧时间戳回调,一般播放器无时间戳回调需求的话,无需设置:
//video timestamp callback
video_frame_ts_callback_ = new SP_SDKRenderVideoFrameTimestampCallBack(SP_SDKRenderVideoFrameTimestampCallBack);
NTSmartPlayerSDK.NT_SP_SetRenderVideoFrameTimestampCallBack(player_handle_, IntPtr.Zero, video_frame_ts_callback_);
  1. NT_SP_SetAudioPCMFrameCallBack:设置音频PCM帧回调, 吐PCM数据出来,目前每帧大小是10ms,一般播放器无使用需求的话,无需设置;
  1. NT_SP_SetUserDataCallBack:设置用户数据回调,此接口需要和推送端SDK配套使用,用于返回推送端设定的实时用户数据(如时间戳、经纬度等各种扩展指令或信息),如只是单纯使用播放SDK,无需设置;
  1. NT_SP_SetSEIDataCallBack:设置视频SEI数据回调,如只是单纯使用播放SDK,不需要额外处理扩展SEI数据的话,无需设置。

2.4.7 D3DRender检测

目前,几乎很少存在不支持D3D绘制的情况,考虑到系统通用性,我们在播放之前,先做检测,具体调用接口如下:

/*
 * handle: 播放句柄
 * hwnd: 这个要传入真正用来绘制的窗口句柄
 * is_support: 如果支持的话 *is_support 为1, 不支持的话为0
 * 接口调用成功返回NT_ERC_OK
 */
[DllImport(@"SmartPlayerSDK.dll")]
public static extern UInt32 NT_SP_IsSupportD3DRender(IntPtr handle, IntPtr hwnd, ref Int32 is_support);

对于不支持D3D绘制的情况下,设置回调YUV数据,上层直接用GDI模式绘制,注意:GDI绘制效率偏低。

Int32 in_support_d3d_render = 0;

if (NT.NTBaseCodeDefine.NT_ERC_OK == NTSmartPlayerSDK.NT_SP_IsSupportD3DRender(player_handle_, playWnd.Handle, ref in_support_d3d_render))
{
    if (1 == in_support_d3d_render)
    {
        is_support_d3d_render = true;
    }
}

if (is_support_d3d_render)
{
    is_gdi_render_ = false;

    // 支持d3d绘制的话,就用D3D绘制
    NTSmartPlayerSDK.NT_SP_SetRenderWindow(player_handle_, playWnd.Handle);

    if (btn_check_render_scale_mode.Checked)
    {
        NTSmartPlayerSDK.NT_SP_SetRenderScaleMode(player_handle_, 1);
    }
    else
    {
        NTSmartPlayerSDK.NT_SP_SetRenderScaleMode(player_handle_, 0);
    }
}
else
{
    is_gdi_render_ = true;
    playWnd.Visible = false;

    // 不支持D3D就让播放器吐出数据来,用GDI绘制

    //video frame callback (YUV/RGB)
    //format请参见 NT_SP_E_VIDEO_FRAME_FORMAT,如需回调YUV,请设置为 NT_SP_E_VIDEO_FRAME_FROMAT_I420
    video_frame_call_back_ = new SP_SDKVideoFrameCallBack(SetVideoFrameCallBack);
NTSmartPlayerSDK.NT_SP_SetVideoFrameCallBack(player_handle_, (Int32)NT.NTSmartPlayerDefine.NT_SP_E_VIDEO_FRAME_FORMAT.NT_SP_E_VIDEO_FRAME_FORMAT_RGB32, IntPtr.Zero, video_frame_call_back_);
}

2.4.8 设置播放URL

NT_SP_SetURL:支持rtsp/rtmp/本地FLV文件(全路径)。

2.4.9 设置回调PCM

NT_SP_SetIsOutputAudioDevice:设置是否播放出声音,这个和静音接口是有区别的,这个接口的主要目的是为了用户设置了外部PCM回调接口后,又不想让SDK播放出声音时使用。

2.4.10 RTMP/RTSP播放参数设置

具体可参照Demo源码里面InitCommonSDKParam():

2.4.10.1 播放前可选设置接口

  1. NT_SP_SetBuffer:设置视频播放缓冲buffer大小,单位:毫秒;
  2. NT_SP_SetRTSPTcpMode:设置RTSP TCP 模式, 1为TCP, 0为UDP, 此接口仅RTSP有效;
  3. NT_SP_SetRtspTimeout:设置RTSP超时时间, timeout单位为秒,必须大于0;
  4. NT_SP_SetRtspAutoSwitchTcpUdp:对于RTSP来说,有些可能支持rtp over udp方式,有些可能支持使用rtp over tcp方式. 为了方便使用,有些场景下可以开启自动尝试切换开关, 打开后如果udp无法播放,sdk会自动尝试tcp, 如果tcp方式播放不了,sdk会自动尝试udp, is_auto_switch_tcp_udp: 如果设置1的话, sdk将在tcp和udp之间尝试切换播放,如果设置为0,则不尝试切换;
  5. NT_SP_SetFastStartup:设置秒开, 1为秒开, 0为不秒开,此接口用于如RTMP服务器缓存GOP时,酌情使用;
  6. NT_SP_SetLowLatencyMode:设置低延时播放模式,默认是正常播放模式,mode: 1为低延时模式, 0为正常模式,低延迟模式下,可能会导致音视频不同步,或视频帧不均匀;
  7. NT_SP_SetReportDownloadSpeed:设置下载速度上报, 默认不上报下载速度;

* is_report: 上报开关, 1: 表上报. 0: 表示不上报. 其他值无效.

* report_interval: 上报时间间隔(上报频率),单位是秒,最小值是1秒1次. 如果小于1且设置了上报,将调用失败

* 注意:如果设置上报的话,请设置SetEventCallBack, 然后在回调函数里面处理这个事件.

* 上报事件是:NT_SP_E_EVENT_ID_DOWNLOAD_SPEED

  1. NT_SP_GetDownloadSpeed:主动获取下载速度,speed: 返回下载速度,单位是Byte/s;
  2. NT_SP_SetParam:万能接口, 设置参数, 大多数问题, 这些接口都能解决;
  3. NT_SP_GetParam:万能接口, 得到参数, 大多数问题,这些接口都能解决;

2.4.10.2 播放前后可实时调用的接口

  1. NT_SP_SetMute:播放过程中,实时静音、取消静音,可播放之前调用,亦或播放过程中实时调用;
  2. NT_SP_SetAudioVolume:不同于实时静音接口,此接口可以更细粒度的控制音量,默认范围[0,100],其中0是静音,100是最大音量, 默认是100;
  3. NT_SP_SetOnlyDecodeVideoKeyFrame:多窗口播放场景下,部分窗口可能只需要播放关键帧,如有类似场景需求,可用此接口;
  4. NT_SP_SetRotation:设置视频View旋转,顺时针旋转,degress: 设置0, 90, 180, 270度有效,其他值无效,注意:除了0度,其他角度播放会耗费更多CPU;
  5. NT_SP_SetFlipVertical:设置视频View上下反转(垂直反转);
  6. NT_SP_SetFlipHorizontal:设置视频View水平反转;
  7. NT_SP_SetRenderScaleMode:设置视频画面的填充模式,如填充整个绘制窗口、等比例填充绘制窗口,如不设置,默认填充整个绘制窗口;

2.4.11 开始播放

NT_SP_StartPlay

开始播放RTMP或RTSP流数据。

2.4.12 RTMP/RTSP拉流端录像

  1. NT_SP_SetRecorderDirectory:设置录像目录
  2. NT_SP_SetRecorderFileMaxSize:设置单个文件最大大小
  3. NT_SP_SetRecorderFileNameRuler:设置录像文件名生成规则
  4. NT_SP_SetRecorderCallBack:设置录像回调接口
  5. NT_SP_SetRecorderAudioTranscodeAAC:设置录像时音频转AAC编码的开关, aac比较通用,sdk增加其他音频编码(比如speex, pcmu, pcma等)转aac的功能
  6. NT_SP_SetRecorderVideo:设置是否录视频,默认的话,如果视频源有视频就录,没有就没得录, 但有些场景下可能不想录制视频,只想录音频,所以增加个开关
  7. NT_SP_SetRecorderAudio:设置是否录音频,默认的话,如果视频源有音频就录,没有就没得录, 但有些场景下可能不想录制音频,只想录视频,所以增加个开关
  8. NT_SP_StartRecorder:启动录像
  9. NT_SP_StopRecorder:停止录像

2.4.13 实时快照

NT_SP_CaptureImage

用于播放端实时截取当前播放图片,图片以PNG形式保存至本地。

String name = capture_image_path_ + "\\" +  DateTime.Now.ToString("hh-mm-ss") + ".png";

byte[] buffer1 = Encoding.Default.GetBytes(name);
byte[] buffer2 = Encoding.Convert(Encoding.Default, Encoding.UTF8, buffer1, 0, buffer1.Length);

byte[] buffer3 = new byte[buffer2.Length + 1];
buffer3[buffer2.Length] = 0;

Array.Copy(buffer2, buffer3, buffer2.Length);

IntPtr file_name_ptr = Marshal.AllocHGlobal(buffer3.Length);
Marshal.Copy(buffer3, 0, file_name_ptr, buffer3.Length);

capture_image_call_back_ = new SP_SDKCaptureImageCallBack(SDKCaptureImageCallBack);

UInt32 ret = NTSmartPlayerSDK.NT_SP_CaptureImage(player_handle_, file_name_ptr, IntPtr.Zero, capture_image_call_back_);

Marshal.FreeHGlobal(file_name_ptr);

if (NT.NTBaseCodeDefine.NT_ERC_OK == ret)
{
    // 发送截图请求成功
}
else if ((UInt32)NT.NTSmartPlayerDefine.SP_E_ERROR_CODE.NT_ERC_SP_TOO_MANY_CAPTURE_IMAGE_REQUESTS == ret)
{
    // 通知用户延时
    MessageBox.Show("Too many capture image requests!");
}
else
{
    // 其他失败
}

2.4.14 快速切换URL

NT_SP_SwitchURL

快速切换URL,用于不用析构整个player实例的前提下,实时切换播放的URL。

2.4.15 用户数据回调

NT_SP_SetUserDataCallBack

设置用户数据回调,用于接收扩展SEI模块发送的用户数据信息,如不是配合我们扩展SEI发送DK,此接口无需调用。

2.4.16 SEI数据回调

NT_SP_SetSEIDataCallBack

设置视频sei数据回调,用于接收SEI数据回调,如流数据不存在SEI或不准备处理SEI数据,此接口无需调用。

2.4.17 停止播放

NT_SP_StopPlay

停止播放RTMP或RTSP流数据。

2.4.18 关闭播放实例

NT_SP_Close

调用Close接口后,player handler置空。

if ( player_handle_ != IntPtr.Zero)
{
     NTSmartPlayerSDK.NT_SP_Close(player_handle_);
     player_handle_ = IntPtr.Zero;
}

2.4.19 Uninit

NT_SP_UnInit

UnInit() 是SDK最后一个调用的接口,多实例环境下,只需要调用一次即可。