关于rtmp:LAL-v0330发布支持抓取流数据回放调试

Go语言流媒体开源我的项目 LAL 明天公布了v0.33.0版本。 LAL 我的项目地址:https://github.com/q191201771...老规矩,先挑三个批改简略介绍一下: ▌ 一,反对抓取流数据回放调试这个性能能够大幅提高lal用户反馈问题、定位问题的效率,从而进步lal的兼容性。 详情我独自写了一篇文档: 《debug dump抓取lal流调试》 ▌ 二,HTTP-API和HTTP-Notify回调为HLS协定提供更丰盛的信息咱们都晓得,HLS是客户端持续性发动一个又一个HTTP申请,每次获取一个TS流片段文件的形式来播放的。这种短连贯的办法导致服务端不好辨别不同的播放者,也不好判断播放者开始播放、完结播放的工夫点。 此次,咱们通过减少302跳转,给m3u8 url减少一个带用户惟一ID的参数,并且给一个用户的多个TS都减少上该ID,从而反对: 获取hls播放者数量、码率等信息获取hls开始播放、完结播放的事件告诉这个性能在配置文件有两个相干的配置,具体见 hls/sub_session_timeout_ms 和 hls/sub_session_hash_key 如果你不喜爱m3u8 302跳转带来的开销,你也不关怀hls的统计与告诉的信息,那么你就能够通过配置文件将该性能敞开。 相干文档:《lalserver HTTP API接口》《lalserver HTTP Notify(Callback/Webhook)事件回调》▌ 三,解析rtp extension扩大头减少了解析rtp扩大头的逻辑,使得能够更好的反对rtsp流。 ▌ 更多批改还有一些批改不一一介绍了,大抵如下: [chore] docker同时反对amd和arm架构[feat] demo: analyseflv反对http flv流或flv文件作为输出[feat] 插件化例子:减少读取flv文件再通过CustomPubSession将数据输出lalserver的例子[opt] rtmp: 放大打chunk时预调配的内存大小[opt] 插件化:Cutsomize Pub反对AvPacket、RtmpMsg两种输出数据的形式[opt] Gop缓冲性能反对配置单个Gop内的最大缓冲帧数量[fix] 修复hls获取app name失败的问题[fix] flv: 修复ReadAllTagsFromFlvFile中没有敞开文件的bug[fix] rtmp: 接管buff解析前无效长度查看以上内容摘取自 《lal CHANGELOG版本日志》 ,你能够通过源文档获取更具体的内容。 进一步理解lalgithub官网文档分割作者本文完,祝你明天开心。

February 13, 2023 · 1 min · jiezi

关于rtmp:流媒体H265使用RTMP推流的完整过程记录

次要包含如下三个局部装置: 1. Nginx-rtmp模块:PingOS装置应用docker进行装置:reference # 拉取dockerfilegit clone https://github.com/pingostack/pingos.gitcd docker# 应用shell脚本产生docker容器(默认为docker_pingos_1)chmod +x run.sh./run.sh build。/run.sh up -d2. 本地装置H264 H265依照ffmpeg官网指南装置H264 H265 # 装置须要的库sudo apt-get update -qq && sudo apt-get -y install \ autoconf \ automake \ build-essential \ cmake \ git-core \ libass-dev \ libfreetype6-dev \ libgnutls28-dev \ libsdl2-dev \ libtool \ libva-dev \ libvdpau-dev \ libvorbis-dev \ libxcb1-dev \ libxcb-shm0-dev \ libxcb-xfixes0-dev \ meson \ ninja-build \ pkg-config \ texinfo \ wget \ yasm \ zlib1g-dev# 装置h264 h265sudo apt-get install libx264-devsudo apt-get install libx265-dev libnuma-dev3. 本地装置ffmpeg谬误状况参考 reference ...

September 19, 2021 · 1 min · jiezi

关于rtmp:LAL开源Go语言音视频流媒体服务器

https://github.com/q191201771... 中文文档 LAL is an audio/video live streaming broadcast server written in Go. It's sort of like nginx-rtmp-module, but easier to use and with more features, e.g RTMP, RTSP(RTP/RTCP), HLS, HTTP[S]-FLV/HTTP-TS, WebSocket-FLV/TS, H264/H265/AAC, relay, cluster, record, HTTP API/Notify, GOP cache. And more than a server, act as package and client InstallThere are 2 ways of installing lal. Prebuilt binariesPrebuilt binaries for Linux, macOS(Darwin), Windows are available in the lal github releases page. Naturally, using the latest release binary is the recommended way. The naming format is lal_<version>_<platform>.zip, e.g. lal_v0.20.0_linux.zip ...

April 24, 2021 · 2 min · jiezi

mac实现局域网rtmp推拉流直播

mac实现局域网rtmp直播主要分为6步,原理就是我们要搭建一个rtmp服务器,安装FFmpeg。然后通过FFmpeg推流到这个服务器上,最后局域网内用vlc去拉流观看就可以了。 搭建rtmp服务器搭建rtmp服务器我们就不手动来做了,因为比较复杂。我们使用docker几行命令就能搞定。 安装FFmpeg,我们使用homebrew来安装。安装homebrew,打开https://brew.sh/,然后copy网址里的命令输入到命令中回车。安装完homebrew之后,我们来安装FFmpeg,执行命令brew install ffmpeg安装docker。直接官网下载dmg安装就可以了。启动docker,分别执行命令,这里用到的是alfg/nginx-rtmp库。docker pull alfg/nginx-rtmpdocker run -it -p 1935:1935 -p 8080:80 --rm alfg/nginx-rtmp推流。直播推流地址 rtmp://<本机ip>:1935/stream/自己取个名字推流方式ffmpeg -re -i /Users/1.mp4 -c copy -f flv rtmp://<本机ip>:1935/stream/自己取个名字记得把/Users/1.mp4替换成你自己的文件路径拉流,安装vlc软件。在vlc中填入拉流地址rtmp://<本机ip>:1935/stream/自己取个名字

October 16, 2019 · 1 min · jiezi

vue-typescript-videojs-流媒体播放-视频监控

视频才用流媒体,有后台实时返回数据, 要支持flash播放, 所以需安装对应的flash插件。当视频播放时,每间隔3秒向后台发送请求供检测心跳,表明在线收看状态,需要后台持续发送视频数据。 1. yarn add video.js videojs-flash2. 创建videp.js声明文件 3. 创建video_player.vue组件,供外部调用。源码如下<script lang="ts">import { Component, Emit, Prop, Vue } from 'vue-property-decorator';import 'video.js/dist/video-js.css';import _videojs from 'video.js';const videojs = (window as any).videojs || _videojs;import 'videojs-flash';@Component({ name: 'video-player',})export default class VideoPlayer extends Vue { /* ------------------------ INPUT & OUTPUT ------------------------ */ @Prop({ type: Object, default: () => {}}) private options!: object; /* ------------------------ VUEX (vuex getter & vuex action) ------------------------ */ /* ------------------------ LIFECYCLE HOOKS (created & mounted & ...) ------------------------ */ private mounted() { this.player = videojs(this.$refs.videoPlayer, this.options, function onPlayerReady() { // console.log('onPlayerReady'); }); } private beforeDestroy() { if (this.player) { this.player.dispose(); } } /* ------------------------ COMPONENT STATE (data & computed & model) ------------------------ */ private player: any; /* ------------------------ WATCH ------------------------ */ /* ------------------------ METHODS ------------------------ */}</script><template><div class="module_video_player"> <video ref="videoPlayer" class="video-js" autoplay></video></div></template><style lang="stylus" scoped>@import '~@/assets/styles/var.styl';.module_video_player position relative width 780px</style>4. 在需要使用的模块(如show_monitor.vue)调用。组件创建后,向后台发送轻轻获取rtmp视频播放地址,并更新videoOptions中的src。触发video_player的创建、挂载等。import VideoPlayer from '@/components/video_player.vue';components: { VideoPlayer,} private videoOptions = { techOrder: ['flash', 'html5'], sourceOrder: true, flash: { hls: { withCredentials: false }, }, html5: { hls: { withCredentials: false } }, sources: [{ type: 'rtmp/flv', src: '', // 'rtmp://live.hkstv.hk.lxdns.com/live/hks2', // 香港卫视,可使用此地址测试 }], autoplay: true, controls: true, width: '778', height: '638', };<video-player :options="videoOptions" v-if="videoOptions.sources[0].src !== ''"></video-player>5. 心跳检测在show_monitor.vue创建时,新建定时器,每隔3秒向后台发送一个包含当前监控设备id的请求,告知后台此设备监控被调用播放。show_monitor.vue销毁时,清空定时器,后台将停止传输视频数据。 ...

July 5, 2019 · 2 min · jiezi

通过-wireshark-抓包了解直播流媒体-RTMP-协议基本过程

作者:Elias Zhang 声网资深工程师,拥有从Iaas层的基础信息存储服务到paas层的云服务的职业经历,喜欢python语言,习惯使用C#,熟悉基于和结合CDN的业务产品架构,点播、直播、云导播等。喜欢探索问题和研究创新,拥有5项国家发明专利。在直播是最常见的实时音视频场景,而 RTMP 是该场景下最重要的协议之一,是很多初步接触实时音视频的开发者需要了解的。本文会一边利用 winshark工具进行抓包,一边从中分析 RTMP 协议的基本原理,帮助大家更容易地理解它。 先给出RTMP协议的原文件 https://www.adobe.com/devnet/... 需要用到的时候可以参考一下~。 做推流直播接触最多的并且最主要是RTMP协议 RTMP协议是应用层协议,是要靠底层可靠的传输层(TCP)协议(通常是TCP)来保证信息传输的可靠性的。在基于传输层协议的链接建立完成后,RTMP协议也要客户端和服务器通过“握手”来建立基于传输层链接之上的RTMP Connection链接. 播放一个RTMP协议的流媒体需要经过以下几个步骤:握手,建立网络连接,建立网络流,播放。服务器和客户端之间只能建立一个网络连接,但是基于该连接可以创建很多网络流。他们的关系如图所示: RTMP协议传输时会对数据做自己的格式化,这种格式的消息我们称之为RTMP Message,而实际传输的时候为了更好地实现多路复用、分包和信息的公平性,发送端会把Message划分为带有Message ID的Chunk,每个Chunk可能是一个单独的Message,也可能是Message的一部分,在接受端会根据chunk中包含的data的长度,message id和message的长度把chunk还原成完整的Message,从而实现信息的收发。协议本身的详细字段和流程就不在这里详细解释了,主要结合看包直观的了解下这个协议的流程,更详细的内容可以查阅前面给出的官方文档。 RTMP步骤: 1. 握手 要建立一个有效的RTMP Connection链接,首先要“握手”:客户端要向服务器发送C0,C1,C2(按序)三个chunk,服务器向客户端发送S0,S1,S2(按序)三个chunk,然后才能进行有效的信息传输。RTMP协议本身并没有规定这6个Message的具体传输顺序,但RTMP协议的实现者需要保证这几点: 客户端要等收到S1之后才能发送C2客户端要等收到S2之后才能发送其他信息(控制信息和真实音视频等数据)服务端要等到收到C0之后发送S1服务端必须等到收到C1之后才能发送S2服务端必须等到收到C2之后才能发送其他信息(控制信息和真实音视频等数据)握手开始于客户端发送C0、C1块。服务器收到C0或C1后发送S0和S1。当客户端收齐S0和S1后,开始发送C2。当服务器收齐C0和C1后,开始发送S2。当客户端和服务器分别收到S2和C2后,握手完成。 实际上真实发包如下: 我们可以看见TCP的三次握手,RTMP基于TCP的可靠传输。 接下去过滤rtmpt协议,rtmp的握手过程如下,我们发现真实发包是C0+C1一起发;S0,S1,S2一起发。 2. 建立网络连接(NetConnection) a) 客户端发送命令消息中的“连接”(connect)到服务器,请求与一个服务应用实例建立连接。 打开connect这个包,一个OSI5层协议模型,最后一个是RTMP协议发送了connect链接消息,查看内容包含推流地址名,但是可以观察到还没有发流名,地址是有app名。 观察一下RTMP的包头 StreamID是每个消息的唯一标识,划分成Chunk和还原Chunk为Message的时候都是根据这个ID来辨识是否是同一个消息的Chunk的,这里面为0说明这个消息是初始的0消息。 Chunk stream ID:message会拆分成多个chunk,同一个Chunk Stream ID必然属于同一个Message。 message type id(消息的类型id):表示实际发送的数据的类型,如8代表音频数据、9代表视频数据。 Format:指的是chunk type。共有4种不同的格式,其中第一种格式字段为0,可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk的差量化的表示,因此可以更简洁地表示相同的数据,实际使用的时候还是应该采用尽量少的字节表示相同意义的数据。因为type0是表示不同数据,其他是差量,所以可以想象如果搜不到type0的包说明这个流肯定有问题。可以通过“rtmpt.header.format == 0”过滤。 b) 服务器接收到连接命令消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到客户端,同时连接到连接命令中提到的应用程序。 c) 服务器发送设置带宽协议消息到客户端。 d) 客户端处理设置带宽协议消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到服务器端。 ...

June 13, 2019 · 1 min · jiezi

理解RTMP协议——chunk格式

本文梳理了理解RTMP协议的基本概念访问我的博客了解更多RTMP 的 message 与 chunkmessage 是 RTMP 中的 M,是消息的单位RTMP Message Header +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Message Type| Payload length| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Timestamp | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Stream ID | +-+-+-+-+-+-+-+-+-+-+-+-+Message Type(1 byte),消息类型很重要,它代表了这个消息是什么类型,当写程序的时候需要根据不同的消息,做不同的处理。Payload length(3 bytes),表示负载的长度(big-endian 格式)Timestamp (4 bytes),时间戳(big-endian 格式),超过最大值后会翻转Stream ID (3 bytes) ,消息流ID(big-endian 格式),用于区分不同流的消息。Message Payload,真实的数据message 的类型消息主要分为三类: 协议控制消息、数据消息、命令消息等协议控制消息Message Type ID = 16,主要用于协议内的控制数据消息Message Type ID = 8 9 188: Audio 音频数据9: Video 视频数据18: Metadata 包括音视频编码、视频宽高等信息。命令消息 Command Message (20, 17)此类型消息主要有 NetConnection 和 NetStream 两个类,两个类分别有多个函数,该消息的调用,可理解为远程函数调用。更多的了解见 Adobe’s Real Time Messaging Protocol 的 5.4 章节Chunk —— 网络中实际发送的内容rtmp 的 message 会切分为 n 个 chunk,再通过 tcp 协议传输为什么 rtmp 基于 tcp 协议,tcp 协议已经有化整为零的方式, rtmp 还需要将 message 划分更小的单元 chunk 呢?分析原因:tcp 协议划分一个个 tcp 报文,是为了在网络传输层上保障数据连续性,丢包重发等特性rtmp 划分 chunk 消息快,是为了在网络应用层上实现低延迟的特性,防止大的数据块(如视频数据)阻塞小的数据块(如音频数据或控制信息)RTMP 的 chunk 设计思想在互联网中传输数据时, 消息(Message)会被拆分成更小的单元, 称为消息块(Chunk)。RTMP Chunk Stream 层级允许在Message stream 层次,将大消息切割成小消息,这样可以避免大的低优先级的消息(如视频消息)阻塞小的高优先级的消息(如音频消息或控制消息)。重复强调,RTMP 是设计用来多路复用的特点,传输内容有视频、音频、控制命令。其中一个非常重要的概念是 multiplexing (复用)不同类型的消息会被分配不同的优先级,当网络传输能力受限时,优先级用来控制消息在网络底层的排队顺序。比如当客户端网络不佳时,流媒体服务器可能会选择丢弃视频消息,以保证音频消息可及时送达客户端。Chunk 的大小设置,通过 Message Type = 1 的控制消息声明如果 message length 大于 max chunk size,则需要将这个message切分为多个 chunk 。前面几个 chunk size 必须是 max size,最后一个就是剩余的大小。以上图为例,Message大小为 300 bytes,默认Chunk size 为 128 bytes,进行拆分成chunk的过程。接下来,探寻 chunk 的结构RTMP Chunk Header +————-+—————-+——————-+———–+ | Basic header|Chunk Msg Header|Extended Time Stamp|Chunk Data | +————-+—————-+——————-+———–+ 1 byte (0,3,7,11 byte) (0,4 byte)设计基于TCP协议的上层协议时,为了防止粘包问题,一般的方法有:1、使用分隔符; 2、在报文header中声明长度。RTMP Chunk Basic Header (1 byte) +-+-+-+-+-+-+-+-+-+-+-+-+-+- | format | chunk stream id | +-+-+-+-+-+-+-+-+-+-+-+-+-+- 2 bits 6 bits RTMP Chunk Header 的长度不是固定的,由RTMP Chunk Basic Header 前2位二进制决定,有4种类型。chunk stream id 的范围 3~65599,02作为保留。format = 00,Chunk Header length = 12 bytes,在一个 chunk 流的开始、时间戳返回的时候必须有这种块,比如:onMetaData, 音视频流刚开始的绝对时间戳,控制消息Basic header + Chunk Msg Header +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | format | chunk stream id | timestamp | message length | msg type id | msg stream id | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 2 bits 6 bits 3 bytes 3 bytes 1 bytes 4 bytesformat = 01,Chunk Header length = 8 bytes,对于可变大小消息的chunk流,在第一个消息之后的每个消息的第一个块应该使用这个格式Basic header + Chunk Msg Header +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | format | chunk stream id | timestamp | message length | msg type id | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 2 bits 6 bits 3 bytes 3 bytes 1 bytes format = 10,Chunk Header length = 4 bytes,对于固定大小消息的chunk流,在第一个消息之后的每个消息的第一个块应该使用这个格式Basic header + Chunk Msg Header +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- | format | chunk stream id | timestamp | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- 2 bits 6 bits 3 bytes format = 11,Chunk Header length = 1 bytes,当一个消息被分成多个块,除了第一块以外,所有的块都应使用这种类型Basic header + Chunk Msg Header +-+-+-+-+-+-+-+-+-+-+-+-+-+- | format | chunk stream id | +-+-+-+-+-+-+-+-+-+-+-+-+-+- 2 bits 6 bits 注意 timestamp 的长度为 3 bytes,当 timestamp 被设置为 0x00ffffff,chunk header 会加上 Extended Time Stamp 字段,否则 Extended Time Stamp 不会出现。因为一个流当中可以传输多个Chunk,那么多个Chunk怎么标记同属于一个 Message 的呢?是通过Chunk Stream ID 区分的,同一个Chunk Stream ID 必然属于同一个 Message因为TCP的有序,所以同一个 Message 中不同的 Chunk 会先后抵达。协议控制消息的chunkMessage type 1~6:1,Set Chunk Size 设置块的大小,通知对端用使用新的块大小,共4 bytes +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Basic Header|Message Header|Ex Timestamp|Set chunk size | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+2,Abort Message 取消消息,用于通知正在等待接收块以完成消息的对等端,丢弃一个块流中已经接收的部分并且取消对该消息的处理,共4 bytes。 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Basic Header|Message Header|Ex Timestamp|Chunk Stream ID | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+3,Acknowledgement 确认消息,客户端或服务端在接收到数量与窗口大小相等的字节后发送确认消息到对方。窗口大小是在没有接收到接收者发送的确认消息之前发送的字节数的最大值。服务端在建立连接之后发送窗口大小。本消息指定序列号。序列号,是到当前时间为止已经接收到的字节数。共4 bytes。 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Basic Header|Message Header|Ex Timestamp| Sequence Number| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+4, User Control Message 用户控制消息,客户端或服务端发送本消息通知对方用户的控制事件。本消息承载事件类型和事件数据。消息数据的头两个字节用于标识事件类型。事件类型之后是事件数据。事件数据字段是可变长的。 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-++-+-+-+ |Basic Header|Message Header|Ex Timestamp| Event Type| Event Data| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-++-+-+-+5,Window Acknowledgement Size 确认窗口大小,客户端或服务端发送本消息来通知对方发送确认(致谢)消息的窗口大小,共4 bytes. ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |Basic Header|Message Header|Ex Timestamp| Acknowledgement Window size | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6,Set Peer Bandwidth 设置对等端带宽,客户端或服务端发送本消息更新对等端的输出带宽。发送者可以在限制类型字段(1 bytes)把消息标记为硬(0),软(1),或者动态(2)。如果是硬限制对等端必须按提供的带宽发送数据。如果是软限制,对等端可以灵活决定带宽,发送端可以限制带宽?。如果是动态限制,带宽既可以是硬限制也可以是软限制。 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |Basic Header|Message Header|Ex Timestamp| Acknowledgement Window size | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | Limit type | ++++++++++++++++ 音频消息的chunkMessage type = 8,Audio message, 客户端或服务端发送本消息用于发送音频数据。消息类型 8 ,保留为音频消息以 FLV ACC 的 RTMP Audio Chunk 为例协议层: 协议层 封装层 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |RTMP Chunk Header | FLV AudioTagHeader | FLV AudioTagBody | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++封装层(FLV):FLV AudioTagHeader ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |SoundFormat | SoundRate | SoundSize | SoundType | AACPacketType | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 bits 2 bits 1 bit 1 bit 8 bits视频消息的chunkMessage type = 9, Video message, 客户端或服务端使用本消息向对方发送视频数据。消息类型值 9 ,保留为视频消息。以 FLV H.264/AVC 的 RTMP Video Chunk 为例协议层: 协议层 封装层 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |RTMP Chunk Header | FLV VideoTagHeader | FLV VideoTagBody | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++封装层(FLV):FLV VideoTagHeader +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |Frame Type | CodecID | AVCPacketType | CompositionTime | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 bits 4 bits 1 byte 3 bytes编码层:FLV VideoTagBody +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | Size | AVCDecoderConfigurationRecord or ( one or more NALUs ) | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 bytesNALU: Network Abstract Layer Unit 网络抽象层单元总结RTMP 低延迟的特性,来自 多路复用,消息分块,消息分优先级 的方法ReferenceAdobe’s Real Time Messaging ProtocolRTMP协议入门网宿超大规模直播运营优化之旅TCP连接那些事RTMP H5直播流技术解析RTMP协议的message ...

March 20, 2019 · 3 min · jiezi

理解RTMP协议——握手连接

本文梳理了理解RTMP协议的基本概念访问我的博客了解更多RTMP 的通信机制rtmp 客户端与服务端通信的机制下图是播放器与 rtmp 服务端通信的例子另外推荐阅读 nginx-rtmp-module 源码,比如,握手协议相关代码在 ngx_rtmp_handshake.c 文件RTMP 的握手连接的例子step 1, tcp 三次握手TCP 握手过程这里不详细展开,参考这篇文章 TCP 的那些事儿step 2, RTMP 握手验证RTMP 握手起到验证的作用,RTMP 握手方式主要分为:简单握手与复杂握手Adobe 协议中描述的是简单握手,而 Adobe 产品 Flash Media Server 采用复杂握手的方式简单握手与流程图有点不同,握手的实际流程分三个步骤:第一步, Client -> Server,内容是 C0+C1第二步, Server -> Client,内容是 S0+S1+S2第三步, Client -> Server,内容是 C2我使用 Wireshark 抓包,验证了过程(我使用 nginx-rtmp-module 做服务器,ffmpeg推流,VLC Media Play播放)报文的解释: C0 与 S0 +-+-+-+-+-+-+-+-+ | version | +-+-+-+-+-+-+-+-+C0 与 S0C0:客户端发送其所支持的 RTMP 版本号:3~31。一般都是写 3S1:服务端返回其所支持的版本号。如果没有客户端的版本号,默认返回 3 C1 与 S1 +-+-+-+-+-+-+-+-+-+-+ | time (4 bytes) | +-+-+-+-+-+-+-+-+-+-+ | zero (4 bytes) | +-+-+-+-+-+-+-+-+-+-+ | random bytes | +-+-+-+-+-+-+-+-+-+-+ |random bytes(cont) | | …. | +-+-+-+-+-+-+-+-+-+-+C1 与 S1C1/S1 长度为 1536B。主要目的是确保握手的唯一性。格式为 time + zero + randomtime 发送时间戳,长度 4 bytezero 保留值 0,长度 4 byterandom 随机值,长度 1528 byte,保证此次握手的唯一性,确定握手的对象 C2 与 S2 +-+-+-+-+-+-+-+-+-+-+ | time (4 bytes) | +-+-+-+-+-+-+-+-+-+-+ | time2(4 bytes) | +-+-+-+-+-+-+-+-+-+-+ | random bytes | +-+-+-+-+-+-+-+-+-+-+ |random bytes(cont) | | …. | +-+-+-+-+-+-+-+-+-+-+C2 与 S2C2/S2 的长度也是 1536B。相当于就是 S1/C1 的响应值,对应 C1/S1 的 Copy 值,在于字段有点区别time, C2/S2 发送的时间戳,长度 4 bytetime2, S1/C1 发送的时间戳,长度 4 byterandom,S1/C1 发送的随机数,长度为 1528BRTMP 是用于网络传输的二进制协议,默认使用 Big-Endian 格式,因为 Big-Endian 格式在抓包时可读性较好复杂握手对于复杂握手,不使用 Adobe 产品 FMS 的话,简单了解即可相对于简单握手,复杂握手增加了严格的验证,主要是 random 字段上进行更细化的划分1528Bytes随机数的部分平均分成两部分,一部分764Bytes存储public key(公共密钥),另一部分764Bytes存储digest(密文,32字节)。从二进制报文的角度,判断复杂握手的特征是,Version部分不为0,服务器端可根据这个来判断是否简单握手或复杂握手。握手的了解到这里,下面继续看看握手之后的步骤step 3, RTMP connectRTMP 有一个重要的概念:Application Instance,直观上,可以体现在 rtmp 的 url 上我测试的推流例子,用到的 url 为: rtmp://192.168.23.152/live/movie大家可以注意到,上面 wireshark 对 rtmp 抓包的截图中,握手后紧接一个 client->server 的报文 connect(’live’),1909 36.398483095 192.168.23.152 192.168.23.152 RTMP 282 connect(’live’)而这个 live 就是这次推流的 Application Instancestep 4, createStream(创建流) — 创建逻辑通道上面 wireshark 对 rtmp 抓包的截图中,有下面两行,第一行是 client->server,第二行是 server->client1933 36.484940956 192.168.23.152 192.168.23.152 RTMP 105 Window Acknowledgement Size 5000000|createStream()1935 36.485004644 192.168.23.152 192.168.23.152 RTMP 109 _result()1946 36.528398367 192.168.23.152 192.168.23.152 RTMP 168 getStreamLength()|play(‘movie’)|Set Buffer Length 1,3000ms直观地,rtmp://192.168.23.152/live/movie 的 movie 是这次拉流的 stream。createStream 命令用于创建逻辑通道,该通道用于传输视频、音频、metadata。在服务器的响应报文 _result() 中会返回Stream ID,用于唯一的标示该Stream。getStreamLength 命令用来获取 movie 的流的长度Real Time Messaging Protocol (AMF0 Command getStreamLength()) RTMP Header RTMP Body String ‘getStreamLength’ Number 3 Null String ‘movie’Real Time Messaging Protocol (AMF0 Command play(‘movie’)) RTMP Header RTMP Body String ‘play’ Number 4 Null String ‘movie’ Number -2000根据 Adobe’s Real Time Messaging Protocol 里对 _result 命令的定义,上面 body 中第四个字段 “Number 1” 便是此次的 Stream IDstep n,anything一般的 rtmp 连接的流程,都如上所示,后面便是命令与音视频数据的消息,比如:播放器的客户端发送play命令来播放指定流,等待服务端传输音视频数据。推流的客户端会发送 publish 命令,开始上传音视频数据。step last, deleteStream(删除流)根据 Adobe’s Real Time Messaging ProtocolNetStream sends the deleteStream command when the NetStream object is getting destroyed.当 NetStream 对象销毁的时候发送删除流命令。比如,播放器客户端停止播放,可以删除指定Stream ID的流。服务器不用对这条命令发送响应报文。ReferenceAdobe’s Real Time Messaging ProtocolRTMP协议入门网宿超大规模直播运营优化之旅TCP连接那些事RTMP H5直播流技术解析RTMP协议的message ...

March 20, 2019 · 2 min · jiezi

理解RTMP协议——简单认识

本文梳理了RTMP协议的基本概念访问我的博客了解更多前言直播行业的兴起,带动了音视频相关技术的发展,本文介绍 RTMP 协议,让人快速理解它。看下面一张视频直播的大体架构图,找找 RTMP 的位置,明白 RTMP 扮演的角色与重要性在上面,RTMP 在视频直播场景的架构中,担任了重要的"血管"般的角色简单介绍RTMP(Real Time Messaging Protocol)实时消息传送协议是Adobe Systems公司为Flash播放器和服务器之间音频、视频和数据传输开发的私有协议。RTMP是一个应用层协议,有多路复用的特点,传输内容有视频、音频、控制命令RTMP 在音视频相关的协议中,它的突出特点是:连接可靠、低延时RTMP 基于 TCPRTMP 是基于TCP的二进制协议,(顺便一提,http为广泛应用的明文协议之一)RTMP 默认端口 1935基于 TCP 的好处提高了RTMP的可靠性基于 TCP 的弊端如果网络条件差时,由于TCP存在重传的机制,所以导致RTMP存在累计延时。当网络状态差时,服务器会将包缓存起来,导致累积的延迟,待网络状况好了,就一起发给客户端。解决 RTMP 累计延时的弊端解决 RTMP 累计延时的弊端的方法:数据下发的角度,服务端上设置对客户端的缓冲区空间大小,一旦累计的缓冲超过限制,服务端就断开连接,迫使客户端在恢复网络时发起重新连接的请求。流媒体推流(上传)的角度,上传流媒体的发布方既可以是服务器也可以是客户端App,发布方发现当前队列中未处理的的视频和音频帧数累计达到一定数目(如50帧),则清空该队列,直接处理最新的实时数据(严格意义上,需要保留关键帧,清除预测帧)。RTMP 的 HTTP 变种与防火墙rtmp 有三个变种:工作在TCP之上的明文协议,使用端口1935RTMPT封装在HTTP请求之中,可穿越防火墙RTMPS类似RTMPT,但使用的是HTTPS连接穿越防火墙的意思是,可能出于安全考虑,互联网中某一些网络(比如小区、校园网)的防火墙限制了http/https以外的协议访问,只允许访问外网ip的 80 端口与 443 端口,或者还允许其他协议,而明文协议的 rtmp 默认端口 1935 不在防火墙开放访问的端口中,无法建立连接。出于现实考虑,使用 http/https 封装 rtmp 协议,增强兼容性。RTMP ServerFMS Wowza (Flash Media Server),商业产品,Adobe公司的产品,license非常昂贵。wowza最突出的特定是多终端适应性,这个在如今多媒体融合的网络环境下有很强的实用意义。以下开源项目:red5, java,比较出名crtmpserver,C++nginx-rtmp-module,nginx的轻量模块,一般后端工程师比较熟悉 nginx,上手方便ReferenceAdobe’s Real Time Messaging ProtocolRTMP协议入门网宿超大规模直播运营优化之旅TCP连接那些事RTMP H5直播流技术解析RTMP协议的message

March 20, 2019 · 1 min · jiezi

基于ffmpeg+nginx+UscreenCapture的局域网直播系统搭建

本文的创作灵感是由Windows screen recording with FFmpeg UScreenCapture and NGINX RTMP module而来,笔者建议大家可以先去原文看一下大致的内容,但需要一定的英文阅读能力,本文并不是照搬翻译,而是根据原文成功搭建直播系统的基础上增加了服务器录制和本地录制的内容:系统环境:Windows 7 Service Pack 1 64bit软件准备:ffmpeg:这里笔者使用原文的地址下载了官方打包好的可执行文件nginx:这里笔者使用原文的地址下载了编译好nginx rtmp module的打包文件UScreenCapture(x64):请参看原文下载地址QtAv:笔者在二次开发时选择的库 (可选)vlc-qt:基于vlc的库(可选),笔者一开始做本地录制功能的时候选择了这个库,但是在尝试本地播放视频同时录制的时候,发现开始录制后没有停止录制的接口,于是就放弃了libvlc:vlc官方的开源组件,可供其他开发平台调用(可选),上面的vlc-qt是基于Qt开发环境的,如果不依赖Qt可以尝试用这个库,不过需要看一下官方的开发文档,笔者并没有相关开发经验,就不做过多介绍了产品需求:将某台PC的桌面作为视频直播源,推送至直播服务器,可供局域网内的其他PC通过视频播放器观看,观看直播的同时可以随时开始/停止录制当前的播放内容开发步骤:安装UScreenCapture(x64),后续作为可用视频输入源供ffmpeg使用点击下载好的nginx目录中的nginx.exe,启动nginx服务器,根据conf文件夹中的nginx.conf配置,此时服务器会监听81-web端口和1935-rtmp端口打开命令提示符(cmd),输入以下命令,此命令用于确认是否正确安装并ffmpeg可以识别UscreenCapture作为视频流输入源:[这里是ffmpeg文件夹的绝对路径]/ffmpeg.exe -list_devices true -f dshow -i dummy[这里是ffmpeg文件夹的绝对路径]/ffmpeg -f dshow -i video=“UScreenCapture” -preset ultrafast -f flv rtmp://127.0.0.1/view/test上述代码的作用就是,以UScreenCapture作为输入源,编码速率设定为ultrafast,编码格式为flv,将视频媒体流推送到rtmp://127.0.0.1/view/test这个地址上当然,其中输入源可以选择摄像头,本地视频文件等;编码速率,格式以及推送地址都可以根据需要进行配置,最终的目的都是为了把本地源推送至服务器以供远程播放器使用测试直播是否正常可以使用ffmpeg文件夹中的ffplay rtmp://127.0.0.1/view/test, 也可以使用其他播放器,如vlc player如果需要录制视频功能,可以选择以下两种方案,录制后的文件各自保存的位置不同播放端录制使用QtAv的VideoDecoder+VideoEncoder+AVMuxer+AVDemuxer,将播放源进行编解码后保存到本地,因为官方没有正式的例子,下文的代码仅供演示使用(代码整理中,后续尽快添加)To Be Continued。。。服务器端录制 利用nginx rtmp module的rtmp_control功能,通过HTTP协议在服务器端进行开始录制及停止录制操作,nginx端的配置见下文server { …… location /control { rtmp_control all; }}rtmp { server { listen 1935; application view {下面的录制命令app参数会用到这个配置名 live on; recorder NAME_THIS_WHAT_YOU_WANT { # 下面的录制命令rec参数会用到这个配置名 record all manual; record_suffix -%Y-%m-%d-%H%M.mp4; record_path C:\nginx\recordings; record_unique on; } } }}rtmp://127.0.0.1/view/test,其中view是服务器配置的rtmp服务名,test是自定义的视频流名称启动录制:http://localhost:81/control/record/start?app=view&name=自定义的视频流名称&rec=NAME_THIS_WHAT_YOU_WANT停止录制:http://localhost:81/control/record/stop?app=view&name=自定义的视频流名称&rec=NAME_THIS_WHAT_YOU_WANT ...

February 13, 2019 · 1 min · jiezi

视频技术详解:RTMP H5 直播流技术解析

本文聚焦 RTMP 协议的最精华的内容,接进行实际操作 Buffer 的练习和协议的学习。RTMP 是什么RTMP 全称即是 Real-Time Messaging Protocol。顾名思义就是用来作为实时通信的一种协议。该协议是 Adobe 搞出来的。主要是用来传递音视频流的。它通过一种自定义的协议,来完成对指定直播流的播放和相关的操作。和现行的直播流相比,RTMP 主要的特点就是高效,这里,我就不多费口舌了。我们先来了解一下 RTMP 是如何进行握手的。RTMP 握手RTMP 是基于 TCP 三次握手之后的,所以,RTMP 不是和 TCP 一个 level 的。它本身是基于 TCP 的可靠性连接。RTMP 握手的方式如图:(C 代表 Client,S 代表 Server)它主要是通过两端的字段内容协商,来完成可信度认证的。基本过程如下:client: 客户端需要发 3 个包。C0,C1,C2server: 服务端也需要发同样 3 个包。 S0,S1,S2。整个过程如上图所述,但实际上有些细节需要注意。握手开始:【1】 客户端发送 C0,C1 包此时,客户端处于等待状态。客户端有两个限制:客户端在未接受到 S1 之前不能发送 C2 包客户端在未接收到 S2 之前不能发送任何实际数据包【2】 服务端在接受到 C0,发送 S0,S1 包。也可以等到接受到 C1 之后再一起发送,C1 包的等待不是必须的。此时,服务端处于等待状态。服务端有两个限制:服务端在未接受到 C1 之前不能发送 S2.服务端在未接收到 C2 之前不能发送任何实际数据包【3】客户端接受到 S1/S0 包后,发送 C2 包。【4】服务端接受到 C2 包后,返回 S2 包,并且此时握手已经完成。不过,在实际应用中,并不是严格按照上面的来。因为 RTMP 并不是强安全性的协议,所以,S2/C2 包只需要 C1/S1 中的内容,就可以完成内容的拼接。这么多限制,说白了,其实就是一种通用模式:C0+C1S0+S1+S2C2接下来,我们来具体看看 C/S 012 包分别代表什么。C0 && S0C0 和 S0 其实区别不大,我这里主要讲解一下 C0,就差不多了。首先,C0 的长度为 1B。它的主要工作是确定 RTMP 的版本号。C0:客户端发送其所支持的 RTMP 版本号:331。一般都是写 3。S1:服务端返回其所支持的版本号。如果没有客户端的版本号,默认返回 3。C1 && S1C1/S1 长度为 1536B。主要目的是确保握手的唯一性。格式为:time: 发送时间戳,这个其实不是很重要,不过需要记住,不要超出 4B 的范围即可。zero: 保留值 0.random: 该字段长尾 1528B。主要内容就是随机值,不管你用什么产生都可以。它主要是为了保证此次握手的唯一性,和确定握手的对象。C2 && S2C2/S2 的长度也是 1536B。相当于就是 S1/C1 的响应值。上图也简单说明了就是,对应 C1/S1 的 Copy 值,不过第二个字段有区别。基本格式为:time: 时间戳,同上,也不是很重要time2: C1/S1 发送的时间戳。random: S1/C1 发送的随机数。长度为 1528B。这里需要提及的是,RTMP 默认都是使用 Big-Endian 进行写入和读取,除非强调对某个字段使用 Little-Endian 字节序。上面握手协议的顺序也是根据其中相关的字段来进行制定的。这样,看起来很容易啊哈,但是,我们并不仅仅停留在了解,而是要真正的了解,接下来,我们来实现一下,如果通过 Buffer 来进行 3 次握手。这里,我们作为 Client 端来进行请求的发起,假设 Server 端是按照标准进行发送即可。Buffer 实操握手我们使用 Buffer 实操主要涉及两块,一个块是 request server 的搭建,还有一块是 Buffer 的拼接。Request Server 搭建这里的 Server 是直接使用底层的 TCP 连接。如下,一个简易的模板:const client = new net.Socket();client.connect({port: 1935,host: “6721.myqcloud.com”},()=>{ console.log(“connected”);});client.on(‘data’,(data)=>{client.write(‘hello’);});不过,为了更好的进行实际演练,我们通过 EventEmitter 的方式,来做一个筛选器。这里,我们使用 mitt 模块来做代理。const Emitter = require(‘mitt’)();然后,我们只要分析的就是将要接受到的 S0/1/2 包。根据上面的字节包图,可以清楚的知道包里面的详细内容。这里,为了简单起见,我们排除其他协议的包头,只是针对 RTMP 里面的包。而且,我们针对的只有 3 种包,S0/1/2。为了达到这种目的,我们需要在 data 时间中,加上相应的钩子才行。这里,我们借用 Now 直播的 RTMP 流来进行相关的 RTMP 直播讲解。Buffer 操作Server 的搭建其实上网搜一搜,应该都可以搜索出来。关键点在于,如何针对 RTMP 的实操握手进行 encode/decode。所以,这里,我们针对上述操作,来主要讲解一下。我们主要的工作量在于如何构造出 C0/1/2。根据上面格式的描述,大家应该可以清楚的知道 C0/1/2 里面的格式分别有啥。比如,C1 中的 time 和 random,其实并不是必须字段,所以,为了简单起见,我们可以默认设为 0。具体代码如下:class C {constructor() { this.time; this.random;}C0() { let buf = Buffer.alloc(1); buf[0] = 3; return buf;}C1() { let buf = Buffer.alloc(1536); return buf;}/** * write C2 package * @param {Number} time the 4B Number of time * @param {Buffer} random 1528 byte */produceC2(){ let buf = Buffer.alloc(1536); // leave empty value as origin time buf.writeUInt32BE(this.time, 4); this.random.copy(buf,8,0,1528); return buf;}get getC01(){ return Buffer.concat([this.C0(),this.C1()]);}get C2(){ return this.produceC2();}}接下来,我们来看一下,结合 server 完成的 RTMP 客户端服务。const Client = new net.Socket();const RTMP_C = new C();Client.connect({port: 1935,host: “6721.liveplay.myqcloud.com”}, () => {console.log(‘connected’)Client.write(RTMP_C.getC01);});Client.on(‘data’,res=>{if(!res){ console.warn(‘received empty Buffer ’ + res); return;}// start to decode res packageif(!RTMP_C.S0 && res.length>0){ RTMP_C.S0 = res.readUInt8(0); res = res.slice(1);}if(!RTMP_C.S1 && res.length>=1536){ RTMP_C.time = res.readUInt32BE(0); RTMP_C.random = res.slice(8,1536); RTMP_C.S1 = true; res = res.slice(1536); console.log(‘send C2’); Client.write(RTMP_C.C2);}if(!RTMP_C.S2 && res.length >= 1536){ RTMP_C.S2 = true; res = res.slice(1536);}})详细代码可以参考 gist。RTMP 基本架构RTMP 整个内容,除了握手,其实剩下的就是一些列围绕 type id 的 message。为了让大家更清楚的看到整个架构,这里简单陈列了一份框架:在 Message 下的 3 个一级子 Item 就是我们现在将要大致讲解的内容。可以看到上面所有的 item 都有一个共同的父 Item–Message。它的基本结构为:Header: header 部分用来标识不同的 typeID,告诉客户端相应的 Message 类型。另外,还有个功效就是多路分发。Body: Body 内容就是相应发送的数据。这个根据不同的 typeID 来说,格式就完全不一样了。下面,我们先了解一下 Header 和不同 typeID 的内容:HeaderRTMP 中的 Header 分为 Basic Header 和 Message Header。需要注意,他们两者并不是独立的,而是相互联系。Message Header 的结构由 Basic Header 的内容来决定。接下来,先分开来讲解:Basic HeaderBH(基础头部)主要是定义了该 chunk stream ID 和 chunk type。需要注意的是,BH 是变长度的,即,它的长度范围是 1-3B。怎么讲呢?就是根据不同的 chunk stream ID 来决定具体的长度。CS ID(Chunk Stream ID)本身的支持的范围为 <= 65597 ,差不多为 22bit。当然,为了节省这 3B 的内容。 Adobe 搞了一个比较绕的理论,即,通过如下格式中的 CS ID 来确定:0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |fmt| cs id | +-+-+-+-+-+-+-+-+即,通过 2-7 bit 位来确定整个 BH 的长度。怎么确定呢?RTMP 规定,CS ID 的 0,1,2 为保留字,你在设置 CS ID 的时候只能从 3 开始。CS ID: 0 ==> 整个 BH 长为 2B,其中可以表示的 Stream ID 数量为 64-319。例如:0 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |fmt| 0 | cs id - 64 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+注意上面的 cs id - 64。这个代表的就是,你通过切割第二个 byte 时,是将得到的值加上 64。即:2th byte + 64 = CS IDCS ID: 1 ==> 整个 BH 长为 3B。可以存储的 Stream ID 数量为 64-65599。例如:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |fmt| 1 | cs id - 64 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+当然,后面 CS ID 的计算方法也是最后的结果加上 64。CS ID >2 ==> 整个 BH 长为 1B。可以存储的 Stream ID 数量为 3-63。例如:0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |fmt| cs id | +-+-+-+-+-+-+-+-+最后强调一下,因为 RTMP 规定,CS ID 的 0,1,2 为保留字,所以,0,1,2 不作为 CS ID。综上所述,CS ID 的起始位为 3(并不代表它是 3 个 Stream)。上面我并没有提到 fmt 字段,这其实是用来定义 Message Header 的。Message Header根据前面 BH 中 fmt 字段的定义,可以分为 4 种 MH(Message Header)。或者说,就是一种 MH 格式会存在从繁到简 4 种:fmt: 0当 fmt 为 0 时,MH 的长度为 11B。该类型的 MH 必须要流的开头部分,这包括当进行快退或者点播时重新获取的流。该结构的整体格式如下:也就是说,当 fmt 为 0 时,其格式是一个完整的 MH。timestamp 是绝对时间戳。用来代表当前流编码。message length: 3B, 发送 message 的长度。type id: 1Bstream id: 4B, 发送 message stream id 的值。是 little-endian 写入格式!fmt: 1当 fmt 为 1 时,MH 的长度为 7B。该类型的 MH 不带 msg stream id。msg stream id 由前面一个 package 决定。该数值主要由前一个 fmt 为 0 的 MH 决定。该类型的 MH 通常放在 fmt 为 0 之后。fmt: 2当 fmt 为 2 时,MH 的长度为 3B。该类型的 MH 只包括一个 timestamp delta 字段。其它的信息都是依照前面一个其他类型 MH 决定的。fmt: 3当 fmt 为 3时,这其实 RTMP 里面就没有了 MH。官方定义,该类型主要全部都是 payload 的 chunk,其 Header 信息和第一个非 type:3 的头一致。因为这主要用于 chunk 中,这些 chunk 都是从一个包里面切割出来的,所以除了第一个 chunk 外,其它的 chunk 都可以采用这种格式。当 fmt 为 3时,计算它的 timestamp 需要注意几点,如果前面一个 chunk 里面存在 timestrameDelta,那么计算 fmt 为 3 的 chunk 时,就直接相加,如果没有,则是使用前一个 chunk 的 timestamp 来进行相加,用代码表示为:prevChunk.timeStamp += prevChunk.timeStampDelta || prevChunk.timeStamp;不过,当 fmt: 3 的情况一般很难遇到。因为,他要求前面几个包必须存在 fmt 为 0/1/2 的情况。接下来的就是 Message Body 部分。Message Body上面说的主要是 Message Header 的公用部分,但是,对于具体的 RTMP Message 来说,里面的 type 会针对不同的业务场景有不同的格式。Message 全部内容如上图所示:这里,我们根据流程图的一级子 item 来展开讲解。PCMPCM 全称为:Protocol Control Messages(协议控制消息)。主要使用来沟通 RTMP 初始状态的相关连接信息,比如,windows size,chunk size 等。PCM 中一共有 5 种不同的 Message 类型,是根据 Header 中的 type ID 决定的,范围是 16 (不包括 4)。另外,PCM 在构造的时候需要注意,它 Heaer 中的 message stream id 和 chunk stream id 需要设置为固定值:message stream ID 为 0chunk stream ID 为 2如图所示:OK,我们接下来一个一个来介绍一下:Set Chunk Size(1)看名字大家应该都能猜到这类信息是用来干啥的。该类型的 PCM 就是用来设置 server 和 client 之间正式传输信息的 chunk 的大小,type ID 为 1。那这有啥用呢?SCS(Set Chunk Size) 是针对正式发送数据而进行数据大小的发送限制。一般默认为 128B。不过,如果 server 觉得太小了,想发送更大的包给你,比如 132B,那么 server 就需要给你发送一个 SCS,告知你,接下来“我发送给你的数据大小是 132B”。0: 只能设为 0 ,用来表示当前的 PCM 的类型。chunk size: 用来表示后面发送正式数据时的大小。范围为 1-16777215。如下,提供过 wireshark 抓包的结果:Abort Message(2)该类 PCM 是用来告诉 client,丢弃指定的 stream 中,已经加载到一半或者还未加载完成的 Chunk Message。它需要指定一个 chunk stream ID。基本格式为:chunk stream id: 指定丢弃 chunk message 的 streamAcknowledgement(3)该协议信息其实就是一个 ACK 包,在实际使用是并没有用到,它主要是用来作为一个 ACK 包,来表示两次 ACK 间,接收端所能接收的最大字节数。它基本格式为:sequence number[4B]: 大小为 4B不过,该包在实际应用中,没有多高的出现频率。Window Acknowledgement Size(5)这是用来协商发送包的大小的。这个和上面的 chunk size 不同,这里主要针对的是客户端可接受的最大数据包的值,而 chunk size 是指每次发送的包的大小。也可以叫做 window size。一般电脑设置的大小都是 500000B。详细格式为:通过,wireshark 抓包的结果为:Set Peer Bandwidth(6)这是 PCM 中,最后一个包。他做的工作主要是根据网速来改变发送包的大小。它的格式和 WAS 类似,不过后面带上了一个 Type 用来标明当前带宽限制算法。当一方接收到该信息后,如果设置的 window size 和前面的 WAS 不一致,需要返回一个 WAS 来进行显示改变。基本格式为:其中 Limit Type 有 3 个取值:0: Hard,表示当前带宽需要和当前设置的 window size 匹配1: Soft,将当前宽带设置为该信息定义的 window size,或者已经生效的 window size。主要取决于谁的 window size 更小2: Dynamic,如果前一个 Limit Type 为 Hard 那么,继续使用 Hard 为基准,否则忽略该次协议信息。实际抓包情况可以参考:UCM全称为:User Control Message(用户控制信息)。它的 Type ID 只能为 4。它主要是发送一些对视频的控制信息。其发送的条件也有一定的限制:msg stream ID 为 0chunk stream ID 为 2它的 Body 部分的基本格式为:UCM 根据 Event Type 的不同,对流进行不同的设置。它的 Event Type 一共有 6 种格式 Stream Begin(0),Stream EOF(1),StreamDry(2),SetBuffer Length(3),StreamIs Recorded(4),PingRequest(6),PingResponse(7)。这里,根据重要性划分,只介绍 Begin,EOF,SetBuffer Length 这 3 种。Stream Begin: Event Type 为 0。它常常出现在,当客户端和服务端成功 connect 后发送。Event Data 为 4B,内容是已经可以正式用来传输数据的 Stream ID(实际没啥用)。Stream EOF: Event Type 为 1。它常常出现在,当音视频流已经全部传输完时。 Event Data 为 4B,用来表示已经发送完音视频流的 Stream ID(实际没啥用)。Set Buffer Length: Event Type 为 3。它主要是为了通知服务端,每毫秒用来接收流中 Buffer 的大小。Event Data 的前 4B 表示 stream ID,后面 4B 表示每毫秒 Buffer 的大小。通常为 3000msOK 剩下就是 Command Msg 里面的内容了。Command MsgCommand Msg 里面的内容,其 type id 涵盖了 8~22 之间的值。具体内容,可以参考下表:需要注意,为什么有些选项里面有两个 id,这主要和 AMF 版本选择有关。第一个 ID 表示 AMF0 的编解码方式,第二个 ID 表示 AMF3 的编解码方式。 其中比较重要的是 command Msg,video,audio 这 3 个 Msg。为了让大家更好的理解 RTMP 流的解析,这里,先讲解一下 video 和 audio 两个 Msg。Video Msg因为 RTMP 是 Adobe 开发的。理所当然,内部的使用格式肯定是 FLV 格式。不过,这和没说一样。因为,FLV 格式内部有很多的 tag 和相关的描述信息。那么,RTMP 是怎么解决的呢?是直接传一整个 FLV 文件,还自定义协议来分段传输 FLV Tag 呢?这个其实很好回答,因为 RTMP 协议是一个长连接,如果是传整个 FLV 文件,根本没必要用到这个,而且,RTMP 最常用在直播当中。直播中的视频都是分段播放的。综上所述,RTMP 是根据自己的自定义协议来分段传输 FLV Tag 的。那具体的协议是啥呢?这个在 RTMP 官方文档中其实也没有给出。它只是告诉我们 Video Msg 的 type ID 是 9 而已。因为,RTMP 只是一个传输工具,里面传什么还是由具体的流生成框架来决定的。所以,这里,我选择了一个非常具有代表性的 RTMP 直播流来进行讲解。通过 wireshark 抓包,可以捕获到以下的 RTMP 包数据:这里需要提及一点,因为 RTMP 是主动将 Video 和 Audio 分开传输,所以,它需要交叉发布 Video 和 Audio,以保证音视频的同步。那么具体每个 Video Data 里面的数据都是一样的吗?如果看 Tag 的话,他们传输的都是 VideoData Tag。先看一下 FLV VideoData Tag 的内容:这是 FLV Video 的协议格式。但,遇到第一个字段 FrameType 的时候,我们就可能懵逼了,这 TM 有 5 种情况,难道 RTMP 会给你 5 种不同的包吗?答案是,有可能,但是,很大情况下,我们只需要支持 1/2 即可。因为,视频中最重要的是 I 帧,它对应的 FrameType 就是 1。而 B/P 则是剩下的 2。我们只要针对 1/2 进行软解,即可实现视频所有信息的获取。所以,在 RTMP 中,也主要(或者大部分)都是传输上面两种 FrameType。我们通过实际抓包来讲解一下。这是 KeyFrame 的包,注意 Buffer 开头的 17 数字。大家可以找到上面的 FrameType 对应找一找,看结果是不是一致的:这是 Inter-frame 的包。同上,大家也可以对比一下:Audio TagAduio Tag 也是和 Video Tag 一样的蜜汁数据。通过观察 FLV Audio Tag 的内容:上面这些字段全是相关的配置值,换句话说,你必须实现知道这些值才行。这里,RTMP 发送 Audio Tag 和 Video Tag 有点不同。因为 Audio Tag 已经不可能再细分为 Config Tag,所以,RTMP 会直接传递 上面的 audio Tag 内容。详细可以参考抓包内容:这也是所有的 Audio Msg 的内容。因为 Audio 和 Video 是分开发送的。所以,在后期进行拼接的时候,需要注意两者的同步。说道这里,顺便补充一下,音视频同步的相关知识点。音视频同步音视频同步简单来说有三种:以 Audio 为准,Video 同步 Audio以 Video 为准,Audio 同步 Video以外部时间戳为准,AV 同时同步主要过程变量参考就是 timeStamp 和 duration。因为,这里主要是做直播的,推荐大家采用第二种方法,以 Video 为准。因为,在实际开发中,会遇到 MP4 文件生成时,必须要求第一帧为 keyframe,这就造成了,以 Audio 为参考的,会遇到两个变量的问题。一个是 timeStamp 一个是 keyframe。当然,解决办法也是有的,就是检查最后一个拼接的 Buffer 是不是 Keyframe,然后判断是否移到下一次同步处理。这里,我简单的说一下,以 Video 为准的同步方法。以 Video 同步,不需要管第一帧是不是 keyframe,也不需要关心 Audio 里面的数据,因为,Audio 数据是非常简单的 AAC 数据。下面我们通过伪代码来说明一下:// known conditionvideo.timeStamp && video.perDuration && video.wholeDurationaudio.timeStamp && audio.perDuration// startrefDuration = video.timeStamp + video.wholeDurationdelta = refDuration - audio.timeStampaudioCount = Math.round(delta/audio.perDuration);audDemuxArr = this._tmpArr.splice(0,audioCount);// begin to demuxthis._remuxVideo(vidDemuxArr);this._remuxAudio(audDemuxArr);上面算法可以避免判断 Aduio 和 Video timeStamp 的比较,保证 Video 一直在 Audio 前面并相差不远。下面,我们回到 RTMP 内容。来看看 Command Msg 里面的内容。Command MsgCommand Msg 是 RTMP 里面的一个主要信息传递工具。常常用在 RTMP 前期和后期处理。Command Msg 是通过 AMF 的格式进行传输的(其实就是类似 JSON 的二进制编码规则)。Command Msg 主要分为 net connect 和 net stream 两大块。它的交流方式是双向的,即,你发送一次 net connect 或者 stream 之后,另外一端都必须返回一个 _result 或者 _error 以表示收到信息。详细结构可以参考下图:后续,我们分为两块进行讲解:netConnectionnetStream里面的 _result 和 _error 会穿插在每个包中进行讲解。NetConnectionnetConnection 可以分为 4 种 Msg,connect,call,createStream,close。connectconnect 是客户端向 Server 端发送播放请求的。里面的字段内容有:Command Name[String]: 默认为 connect。表示信息名称Transaction ID[Number]: 默认为 1。Command Object: 键值对的形式存放相关信息。Optional: 可选值。一般没有那,Command Object 里面又可以存放些什么内容呢?app[String]: 服务端连接应用的名字。这个主要根据你的 RTMP 服务器设定来设置。比如:live。flashver[String]: Flash Player 的版本号。一般根据自己设备上的型号来确定即可。也可以设置为默认值:LNX 9,0,124,2。tcUrl[String]: 服务端的 URL 地址。简单来说,就是 protocol://host/path。比如:rtmp://6521.liveplay.myqcloud.com/live。fpad[Boolean]: 表示是否使用代理。一般为 false。audioCodecs[Number]: 客户端支持的音频解码。后续会介绍。默认可以设置为 4071videoCodecs[Number]: 客户端支持的视频解码。有自定义的标准。默认可以设置为 252videoFunction[Number]: 表明在服务端上调用那种特别的视频函数。默认可以设置为 1简单来说,Command Object 就是起到 RTMP Route 的作用。用来请求特定的资源路径。实际数据,可以参考抓包结果:上面具体的取值主要是根据 rtmp 官方文档来决定。如果懒得查,可以直接使用上面的取值。上面的内容是兼容性比较高的值。当该包成功发送时,另外一端需要得到一个返回包来响应,具体格式为:Command Name[String]: 为 _result 或者 _error。Transaction ID[Number]: 默认为 1。Command Object: 键值对的形式存放相关信息。Information[Object]: 键值对的形式,来描述相关的 response 信息。里面存在的字段有:level,code,description可以参考:connect 包发送的位置,主要是在 RTMP 握手结束之后。如下:callcall 包主要作用是用来远程执行接收端的程序(RPC, remote procedure calls)。不过,在我解 RTMP 的过程中,并没有实际用到过。这里简单介绍一下格式。它的内容和 connect 类似:Procedure Name[String]: 调用处理程序的名字。Transaction ID[Number]: 如果想要有返回,则我们需要制定一个 id。否则为 0。Command Object: 键值对的形式存放相关信息。AMF0/3Optional: 可选值。一般没有Command Object 里面的内容主要是针对程序,设置相关的调用参数。因为内容不固定,这里就不介绍了。call 一般是需要有 response 来表明,远端程序是否执行,以及是否执行成功等。返回的格式为:Command Name[String]: 根据 call 中 Command Object 参数来决定的。Transaction ID[Number]: 如果想要有返回,则我们需要制定一个 id。否则为 0。Command Object: 键值对的形式存放相关信息。AMF0/3Response[Object]: 响应的结果值createStreamcreateStream 包只是用来告诉服务端,我们现在要创建一个 channel 开始进行流的交流了。格式和内容都不复杂:Procedure Name[String]: 调用处理程序的名字。Transaction ID[Number]: 自己制定一个。一般可以设为 2Command Object: 键值对的形式存放相关信息。AMF0/3当成功后,服务端会返回一个 _result 或者 _error 包来说明接收成功,详细内容为:Command Name[String]: 根据 call 中 Command Object 参数来决定的。Transaction ID[Number]: 如果想要有返回,则我们需要制定一个 id。否则为 0。Command Object: 键值对的形式存放相关信息。AMF0/3。一般为 NullStream ID: 返回的 stream ID 值。它的返回值很随意,参考抓包内容:下面,我们来看一下 RTMP 中第二个比较重要的 command msg – netStream msg。NetStream MsgNetStream 里面的 Msg 有很多,但在直播流中,比较重要的只有 play 包。所以,这里我们着重介绍一下 play 包。playplay 包主要是用来告诉 Server 正式播放音视频流。而且,由于 RTMP 天然是做多流分发的。如果遇到网络出现相应的波动,客户端可以根据网络条件多次调用 play 命令,来切换不同模式的流。其基本格式为:Command Name[String]: 根据 call 中 Command Object 参数来决定的。Transaction ID[Number]: 默认为 0。也可以设置为其他值Command Object: 不需要该字段,在该命令中,默认设为 NullStream Name[String]: 用来指定播放的视频流文件。因为,RTMP 天生是支持 FLV 的,所以针对 FLV 文件来说,并不需要加额外的标识,只需要写明文件名即可。比如:StreamName: ‘6721_75994f92ce868a0cd3cc84600a97f75c’不过,如果想要支持其它的文件,那么则需要额外的表示。当然,音频和视频需要不同的支持:如果是播放音频文件,比如 mp3,那么则需要额外的前缀标识符-mp3。例如:mp3:6721_75994f9。如果涉及到视频文件的话,不仅需要前缀,还需要后缀。比如播放的是 MP4 文件,则标识为:mp4:6721_75994f9.mp4。startNumber: 这个字段其实有点意思。它可以分为 3 类来讲解:-2,-1,>=0。-2: 如果是该标识符,服务端会首先寻找是否有对应的 liveStream。没有的话,就找 record_stream。如果还没有的,这次请求会暂时挂起,直到获取到下一次 live_stream。-1: 只有 live_stream 才会播放。=0: 相当于就是 seek video。它会直接找到 record_stream,并且根据该字段的值来确定播放开始时间。如果没有的话,则播放 list 中的下一个 video。durationNumber: 用来设置播放时长的。它里面也有几个参数需要讲解一下,-1,0,>0。-1: 会一直播放到 live_stream 或者 record_stream 结束。0: 会播放一段一段的 frame。一般不用。0: 会直接播放指定 duration 之内的流。如果超出,则会播放指定时间段内容的 record_stream。reset[Boolean]: 该字段没啥用,一般可以忽略。用来表示否是抛弃掉前面的 playlist。整个 play 包内容就已经介绍完了。我们可以看看实际的 play 抓包结果:那 play 包是在那个环节发送,发送完之后需不需要对应的 _result 包呢?play 包比较特殊,它是不需要 _result 回包的。因为,一旦 play 包成功接收后。server 端会直接开始进行 streamBegin 的操作。整个流程为:到这里,后续就可以开始正式接收 video 和 audio 的 stream。转载自https://www.villianhr.com/201… H5 直播流技术解析想要阅读更多技术干货文章,欢迎关注网易云信博客。了解网易云信,来自网易核心架构的通信与视频云服务。网易云信(NeteaseYunXin)是集网易18年IM以及音视频技术打造的PaaS服务产品,来自网易核心技术架构的通信与视频云服务,稳定易用且功能全面,致力于提供全球领先的技术能力和场景化解决方案。开发者通过集成客户端SDK和云端OPEN API,即可快速实现包含IM、音视频通话、直播、点播、互动白板、短信等功能。 ...

January 21, 2019 · 7 min · jiezi

音视频技术傻瓜版解析:带你解锁RTMP

RTMP协议是Real Time Message Protocol(实时信息传输协议)的缩写,它是由Adobe公司提出的一种应用层的协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题。随着VR技术的发展,视频直播等领域逐渐活跃起来,RTMP作为业内广泛使用的协议也重新被相关开发者重视起来。总体介绍RTMP协议是应用层协议,是要靠底层可靠的传输层协议(通常是TCP)来保证信息传输的可靠性的。在基于传输层协议的链接建立完成后,RTMP协议也要客户端和服务器通过“握手”来建立基于传输层链接之上的RTMP Connection链接,在Connection链接上会传输一些控制信息,如SetChunkSize,SetACKWindowSize。其中CreateStream命令会创建一个Stream链接,用于传输具体的音视频数据和控制这些信息传输的命令信息。RTMP协议传输时会对数据做自己的格式化,这种格式的消息我们称之为RTMP Message,而实际传输的时候为了更好地实现多路复用、分包和信息的公平性,发送端会把Message划分为带有Message ID的Chunk,每个Chunk可能是一个单独的Message,也可能是Message的一部分,在接受端会根据chunk中包含的data的长度,message id和message的长度把chunk还原成完整的Message,从而实现信息的收发。握手要建立一个有效的RTMP Connection链接,首先要“握手”:客户端要向服务器发送C0,C1,C2(按序)三个chunk,服务器向客户端发送S0,S1,S2(按序)三个chunk,然后才能进行有效的信息传输。RTMP协议本身并没有规定这6个Message的具体传输顺序,但RTMP协议的实现者需要保证这几点:客户端要等收到S1之后才能发送C2客户端要等收到S2之后才能发送其他信息(控制信息和真实音视频等数据)服务端要等到收到C0之后发送S1服务端必须等到收到C1之后才能发送S2服务端必须等到收到C2之后才能发送其他信息(控制信息和真实音视频等数据) 如果每次发送一个握手chunk的话握手顺序会是这样:理论上来讲只要满足以上条件,如何安排6个Message的顺序都是可以的,但实际实现中为了在保证握手的身份验证功能的基础上尽量减少通信的次数,一般的发送顺序是这样的,这一点可以通过wireshark抓ffmpeg推流包进行验证: |client|Server | |---C0+C1—->| |<--S0+S1+S2– | |---C2----> |RTMP Chunk StreamChunk Stream是对传输RTMP Chunk的流的逻辑上的抽象,客户端和服务器之间有关RTMP的信息都在这个流上通信。这个流上的操作也是我们关注RTMP协议的重点。3.1 Message(消息)这里的Message是指满足该协议格式的、可以切分成Chunk发送的消息,消息包含的字段如下:Timestamp(时间戳):消息的时间戳(但不一定是当前时间,后面会介绍),4个字节Length(长度):是指Message Payload(消息负载)即音视频等信息的数据的长度,3个字节TypeId(类型Id):消息的类型Id,1个字节Message Stream ID(消息的流ID):每个消息的唯一标识,划分成Chunk和还原Chunk为Message的时候都是根据这个ID来辨识是否是同一个消息的Chunk的,4个字节,并且以小端格式存储3.2 Chunking(Message分块)RTMP在收发数据的时候并不是以Message为单位的,而是把Message拆分成Chunk发送,而且必须在一个Chunk发送完成之后才能开始发送下一个Chunk。每个Chunk中带有MessageID代表属于哪个Message,接受端也会按照这个id来将chunk组装成Message。为什么RTMP要将Message拆分成不同的Chunk呢?通过拆分,数据量较大的Message可以被拆分成较小的“Message”,这样就可以避免优先级低的消息持续发送阻塞优先级高的数据,比如在视频的传输过程中,会包括视频帧,音频帧和RTMP控制信息,如果持续发送音频数据或者控制数据的话可能就会造成视频帧的阻塞,然后就会造成看视频时最烦人的卡顿现象。同时对于数据量较小的Message,可以通过对Chunk Header的字段来压缩信息,从而减少信息的传输量。 Chunk的默认大小是128字节,在传输过程中,通过一个叫做Set Chunk Size的控制信息可以设置Chunk数据量的最大值,在发送端和接受端会各自维护一个Chunk Size,可以分别设置这个值来改变自己这一方发送的Chunk的最大大小。大一点的Chunk减少了计算每个chunk的时间从而减少了CPU的占用率,但是它会占用更多的时间在发送上,尤其是在低带宽的网络情况下,很可能会阻塞后面更重要信息的传输。小一点的Chunk可以减少这种阻塞问题,但小的Chunk会引入过多额外的信息(Chunk中的Header),少量多次的传输也可能会造成发送的间断导致不能充分利用高带宽的优势,因此并不适合在高比特率的流中传输。在实际发送时应对要发送的数据用不同的Chunk Size去尝试,通过抓包分析等手段得出合适的Chunk大小,并且在传输过程中可以根据当前的带宽信息和实际信息的大小动态调整Chunk的大小,从而尽量提高CPU的利用率并减少信息的阻塞机率。 3.3 Chunk Format(块格式)3.3.1 Basic Header(基本的头信息)包含了chunk stream ID(流通道Id)和chunk type(chunk的类型),chunk stream id一般被简写为CSID,用来唯一标识一个特定的流通道,chunk type决定了后面Message Header的格式。Basic Header的长度可能是1,2,或3个字节,其中chunk type的长度是固定的(占2位,注意单位是位,bit),Basic Header的长度取决于CSID的大小,在足够存储这两个字段的前提下最好用尽量少的字节从而减少由于引入Header增加的数据量。 RTMP协议支持用户自定义[3,65599]之间的CSID,0,1,2由协议保留表示特殊信息。0代表Basic Header总共要占用2个字节,CSID在[64,319]之间,1代表占用3个字节,CSID在[64,65599]之间,2代表该chunk是控制信息和一些命令信息,后面会有详细的介绍。 chunk type的长度固定为2位,因此CSID的长度是(6=8-2)、(14=16-2)、(22=24-2)中的一个。 当Basic Header为1个字节时,CSID占6位,6位最多可以表示64个数,因此这种情况下CSID在[0,63]之间,其中用户可自定义的范围为[3,63]。当Basic Header为2个字节时,CSID占14位,此时协议将与chunk type所在字节的其他位都置为0,剩下的一个字节来表示CSID-64,这样共有8个字节来存储CSID,8位可以表示[0,255]共256个数,因此这种情况下CSID在[64,319],其中319=255+64。当Basic Header为3个字节时,CSID占22位,此时协议将[2,8]字节置为1,余下的16个字节表示CSID-64,这样共有16个位来存储CSID,16位可以表示[0,65535]共65536个数,因此这种情况下CSID在[64,65599],其中65599=65535+64,需要注意的是,Basic Header是采用小端存储的方式,越往后的字节数量级越高,因此通过这3个字节每一位的值来计算CSID时,应该是:<第三个字节的值>x256+<第二个字节的值>+64可以看到2个字节和3个字节的Basic Header所能表示的CSID是有交集的[64,319],但实际实现时还是应该秉着最少字节的原则使用2个字节的表示方式来表示[64,319]的CSID。3.3.2 Message Header(消息的头信息)包含了要发送的实际信息(可能是完整的,也可能是一部分)的描述信息。Message Header的格式和长度取决于Basic Header的chunk type,共有4种不同的格式,由上面所提到的Basic Header中的fmt字段控制。其中第一种格式可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk的差量化的表示,因此可以更简洁地表示相同的数据,实际使用的时候还是应该采用尽量少的字节表示相同意义的数据。以下按照字节数从多到少的顺序分别介绍这4种格式的Message Header。 Type=0:type=0时Message Header占用11个字节,其他三种能表示的数据它都能表示,但在chunk stream的开始的第一个chunk和头信息中的时间戳后退(即值与上一个chunk相比减小,通常在回退播放的时候会出现这种情况)的时候必须采用这种格式。timestamp(时间戳):占用3个字节,因此它最多能表示到16777215=0xFFFFFF=2 24-1, 当它的值超过这个最大值时,这三个字节都置为1,这样实际的timestamp会转存到Extended Timestamp字段中,接受端在判断timestamp字段24个位都为1时就会去Extended timestamp中解析实际的时间戳。message length(消息数据的长度):占用3个字节,表示实际发送的消息的数据如音频帧、视频帧等数据的长度,单位是字节。注意这里是Message的长度,也就是chunk属于的Message的总数据长度,而不是chunk本身Data的数据的长度。message type id(消息的类型id):占用1个字节,表示实际发送的数据的类型,如8代表音频数据、9代表视频数据。msg stream id(消息的流id):占用4个字节,表示该chunk所在的流的ID,和Basic Header的CSID一样,它采用小端存储的方式, Type = 1:type=1时Message Header占用7个字节,省去了表示msg stream id的4个字节,表示此chunk和上一次发的chunk所在的流相同,如果在发送端只和对端有一个流链接的时候可以尽量去采取这种格式。 timestamp delta:占用3个字节,注意这里和type=0时不同,存储的是和上一个chunk的时间差。类似上面提到的timestamp,当它的值超过3个字节所能表示的最大值时,三个字节都置为1,实际的时间戳差值就会转存到Extended Timestamp字段中,接受端在判断timestamp delta字段24个位都为1时就会去Extended timestamp中解析时机的与上次时间戳的差值。Type = 2:type=2时Message Header占用3个字节,相对于type=1格式又省去了表示消息长度的3个字节和表示消息类型的1个字节,表示此chunk和上一次发送的chunk所在的流、消息的长度和消息的类型都相同。余下的这三个字节表示timestamp delta,使用同type=1Type = 3 0字节!!!好吧,它表示这个chunk的Message Header和上一个是完全相同的,自然就不用再传输一遍了。当它跟在Type=0的chunk后面时,表示和前一个chunk的时间戳都是相同的。什么时候连时间戳都相同呢?就是一个Message拆分成了多个chunk,这个chunk和上一个chunk同属于一个Message。而当它跟在Type=1或者Type=2的chunk后面时,表示和前一个chunk的时间戳的差是相同的。比如第一个chunk的Type=0,timestamp=100,第二个chunk的Type=2,timestamp delta=20,表示时间戳为100+20=120,第三个chunk的Type=3,表示timestamp delta=20,时间戳为120+20=1403.3.3 Extended Timestamp(扩展时间戳)上面我们提到在chunk中会有时间戳timestamp和时间戳差timestamp delta,并且它们不会同时存在,只有这两者之一大于3个字节能表示的最大数值0xFFFFFF=16777215时,才会用这个字段来表示真正的时间戳,否则这个字段为0。扩展时间戳占4个字节,能表示的最大数值就是0xFFFFFFFF=4294967295。当扩展时间戳启用时,timestamp字段或者timestamp delta要全置为1,表示应该去扩展时间戳字段来提取真正的时间戳或者时间戳差。注意扩展时间戳存储的是完整值,而不是减去时间戳或者时间戳差的值。3.3.4 Chunk Data(块数据)用户层面上真正想要发送的与协议无关的数据,长度在(0,chunkSize]之间。3.3.5 chunk表示例1首先包含第一个Message的chunk的Chunk Type为0,因为它没有前面可参考的chunk,timestamp为1000,表示时间戳。type为0的header占用11个字节,假定chunkstreamId为3<127,因此Basic Header占用1个字节,再加上Data的32个字节,因此第一个chunk共44=11+1+32个字节。 第二个chunk和第一个chunk的CSID,TypeId,Data的长度都相同,因此采用Chunk Type=2,timestamp delta=1020-1000=20,因此第二个chunk占用36=3+1+32个字节。 第三个chunk和第二个chunk的CSID,TypeId,Data的长度和时间戳差都相同,因此采用Chunk Type=3省去全部Message Header的信息,占用33=1+32个字节。 第四个chunk和第三个chunk情况相同,也占用33=1+32个字节。 最后实际发送的chunk如下:3.3.6 chunk表示例2注意到Data的Length=307>128,因此这个Message要切分成几个chunk发送,第一个chunk的Type=0,Timestamp=1000,承担128个字节的Data,因此共占用140=11+1+128个字节。 第二个chunk也要发送128个字节,其他字段也同第一个chunk,因此采用Chunk Type=3,此时时间戳也为1000,共占用129=1+128个字节。 第三个chunk要发送的Data的长度为307-128-128=51个字节,还是采用Type=3,共占用1+51=52个字节。 最后实际发送的chunk如下:3.4 协议控制消息(Protocol Control Message) 在RTMP的chunk流会用一些特殊的值来代表协议的控制消息,它们的Message Stream ID必须为0(代表控制流信息),CSID必须为2,Message Type ID可以为1,2,3,5,6,具体代表的消息会在下面依次说明。控制消息的接受端会忽略掉chunk中的时间戳,收到后立即生效。Set Chunk Size(Message Type ID=1):设置chunk中Data字段所能承载的最大字节数,默认为128B,通信过程中可以通过发送该消息来设置chunk Size的大小(不得小于128B),而且通信双方会各自维护一个chunkSize,两端的chunkSize是独立的。比如当A想向B发送一个200B的Message,但默认的chunkSize是128B,因此就要将该消息拆分为Data分别为128B和72B的两个chunk发送,如果此时先发送一个设置chunkSize为256B的消息,再发送Data为200B的chunk,本地不再划分Message,B接受到Set Chunk Size的协议控制消息时会调整的接受的chunk的Data的大小,也不用再将两个chunk组成为一个Message。 以下为代表Set Chunk Size消息的chunk的Data:其中第一位必须为0,chunk Size占31个位,最大可代表2147483647=0x7FFFFFFF=231-1,但实际上所有大于16777215=0xFFFFFF的值都用不上,因为chunk size不能大于Message的长度,表示Message的长度字段是用3个字节表示的,最大只能为0xFFFFFF。Abort Message(Message Type ID=2):当一个Message被切分为多个chunk,接受端只接收到了部分chunk时,发送该控制消息表示发送端不再传输同Message的chunk,接受端接收到这个消息后要丢弃这些不完整的chunk。Data数据中只需要一个CSID,表示丢弃该CSID的所有已接收到的chunk。Acknowledgement(Message Type ID=3):当收到对端的消息大小等于窗口大小(Window Size)时接受端要回馈一个ACK给发送端告知对方可以继续发送数据。窗口大小就是指收到接受端返回的ACK前最多可以发送的字节数量,返回的ACK中会带有从发送上一个ACK后接收到的字节数。Window Acknowledgement Size(Message Type ID=5):发送端在接收到接受端返回的两个ACK间最多可以发送的字节数。Set Peer Bandwidth(Message Type ID=6):限制对端的输出带宽。接受端接收到该消息后会通过设置消息中的Window ACK Size来限制已发送但未接受到反馈的消息的大小来限制发送端的发送带宽。如果消息中的Window ACK Size与上一次发送给发送端的size不同的话要回馈一个Window Acknowledgement Size的控制消息。Hard(Limit Type=0):接受端应该将Window Ack Size设置为消息中的值Soft(Limit Type=1):接受端可以讲Window Ack Size设为消息中的值,也可以保存原来的值(前提是原来的Size小与该控制消息中的Window Ack Size)Dynamic(Limit Type=2):如果上次的Set Peer Bandwidth消息中的Limit Type为0,本次也按Hard处理,否则忽略本消息,不去设置Window Ack Size。不同类型的RTMP Message - Command Message(命令消息,Message Type ID=17或20)表示在客户端盒服务器间传递的在对端执行某些操作的命令消息,如connect表示连接对端,对端如果同意连接的话会记录发送端信息并返回连接成功消息,publish表示开始向对方推流,接受端接到命令后准备好接受对端发送的流信息,后面会对比较常见的Command Message具体介绍。当信息使用AMF0编码时,Message Type ID=20,AMF3编码时Message Type ID=17. - Data Message(数据消息,Message Type ID=15或18):传递一些元数据(MetaData,比如视频名,分辨率等等)或者用户自定义的一些消息。当信息使用AMF0编码时,Message Type ID=18,AMF3编码时Message Type ID=15. - Shared Object Message(共享消息,Message Type ID=16或19):表示一个Flash类型的对象,由键值对的集合组成,用于多客户端,多实例时使用。当信息使用AMF0编码时,Message Type ID=19,AMF3编码时Message Type ID=16. - Audio Message(音频信息,Message Type ID=8):音频数据。 - Video Message(视频信息,Message Type ID=9):视频数据。 - Aggregate Message (聚集信息,Message Type ID=22):多个RTMP子消息的集合 - User Control Message Events(用户控制消息,Message Type ID=4):告知对方执行该信息中包含的用户控制事件,比如Stream Begin事件告知对方流信息开始传输。和前面提到的协议控制信息(Protocol Control Message)不同,这是在RTMP协议层的,而不是在RTMP chunk流协议层的,这个很容易弄混。该信息在chunk流中发送时,Message Stream ID=0,Chunk Stream Id=2,Message Type Id=4。———下面对以上7种信息具体介绍———- 4.1 Command Message(命令消息,Message Type ID=17或20) 发送端发送时会带有命令的名字,如connect,TransactionID表示此次命令的标识,Command Object表示相关参数。接受端收到命令后,会返回以下三种消息中的一种:_result 消息表示接受该命令,对端可以继续往下执行流程,_error消息代表拒绝该命令要执行的操作,method name消息代表要在之前命令的发送端执行的函数名称。这三种回应的消息都要带有收到的命令消息中的TransactionId来表示本次的回应作用于哪个命令。 可以认为发送命令消息的对象有两种,一种是NetConnection,表示双端的上层连接,一种是NetStream,表示流信息的传输通道,控制流信息的状态,如Play播放流,Pause暂停。 4.1.1 NetConnection Commands(连接层的命令) 用来管理双端之间的连接状态,同时也提供了异步远程方法调用(RPC)在对端执行某方法,以下是常见的连接层的命令: 4.1.1.1 connect:用于客户端向服务器发送连接请求,消息的结构如下:第三个字段中的Command Object中会涉及到很多键值对,这里不再一一列出,使用时可以参考协议的官方文档。 消息的回应有两种,_result表示接受连接,_error表示连接失败4.1.1.2 Call:用于在对端执行某函数,即常说的RPC:远程进程调用,消息的结构如下:如果消息中的TransactionID不为0的话,对端需要对该命令做出响应,响应的消息结构如下:4.1.1.3 Create Stream:创建传递具体信息的通道,从而可以在这个流中传递具体信息,传输信息单元为Chunk。4.1.2 NetStream Commands(流连接上的命令)Netstream建立在NetConnection之上,通过NetConnection的createStream命令创建,用于传输具体的音频、视频等信息。在传输层协议之上只能连接一个NetConnection,但一个NetConnection可以建立多个NetStream来建立不同的流通道传输数据。 以下会列出一些常用的NetStream Commands,服务端收到命令后会通过onStatus的命令来响应客户端,表示当前NetStream的状态。 onStatus命令的消息结构如下:用于客户端向服务器发送连接请求,消息的结构如下:如果Pause为true即表示客户端请求暂停的话,服务端暂停对应的流会返回NetStream.Pause.Notify的onStatus命令来告知客户端当前流处于暂停的状态,当Pause为false时,服务端会返回NetStream.Unpause.Notify的命令来告知客户端当前流恢复。如果服务端对该命令响应失败,返回_error信息。4.1.2.1 play(播放):由客户端向服务器发起请求从服务器端接受数据(如果传输的信息是视频的话就是请求开始播流),可以多次调用,这样本地就会形成一组数据流的接收者。注意其中有一个reset字段,表示是覆盖之前的播流(设为true)还是重新开始一路播放(设为false)。4.1.2.2 play2(播放):和上面的play命令不同的是,play2命令可以将当前正在播放的流切换到同样数据但不同比特率的流上,服务器端会维护多种比特率的文件来供客户端使用play2命令来切换。4.1.2.3 deleteStream(删除流):用于客户端告知服务器端本地的某个流对象已被删除,不需要再传输此路流。4.1.2.4 receiveAudio(接收音频):通知服务器端该客户端是否要发送音频receiveAudio命令结构如下:4.1.2.5 receiveVideo(接收视频):通知服务器端该客户端是否要发送视频receiveVideo命令结构如下:4.1.2.6 publish(推送数据):由客户端向服务器发起请求推流到服务器。publish命令结构如下:4.1.2.7 seek(定位流的位置):定位到视频或音频的某个位置,以毫秒为单位。seek命令的结构如下:4.1.2.8 pause(暂停):客户端告知服务端停止或恢复播放。pause命令的结构如下:如果Pause为true即表示客户端请求暂停的话,服务端暂停对应的流会返回NetStream.Pause.Notify的onStatus命令来告知客户端当前流处于暂停的状态,当Pause为false时,服务端会返回NetStream.Unpause.Notify的命令来告知客户端当前流恢复。如果服务端对该命令响应失败,返回_error信息。代表流程5.1 推流流程5.2 播流流程新手建议如果仔细读完了上面讲的RTMP协议,想必会觉得RTMP协议非常繁琐,事实也确实是这样,RTMP协议中充斥着很多冗余的字段,比如三次握手中的时间戳的校对,还有一些特殊的命令,如FCPublish、UnFCPublish等,但在实际实现中为了保证更大兼容性通常还是要处理这些看似多余的命令。加上Adobe对RTMP协议的实现细节有些并没有按照协议来或者协议中没有写清楚自己搞了一套实现,其他应用开发时还要兼容Adobe错误的实现,从而使的RTMP也一直为开发者所诟病。但不管怎样,RTMP确实提供了一种能够全面并且实现简单的协议来保证流信息的传输,这方面暂时还没有一种更完善更简洁的协议能够取代它在视频流开发中的地位。 新人一开始接触RTMP的时候肯定会觉得头大,这也是RTMP协议不简洁的后果。建议读者在学习时先过一遍协议理解大概的概念和流程,然后对照wireshark抓的包,和协议进行比对,这样将理论和实践结合,应该会理解的更快一点。——————— 本文来自 会敲代码的咩 的CSDN 博客网易云信,你身边的即时通讯和音视频技术专家,了解我们,请戳网易云信官网想要阅读更多行业洞察和技术干货,请关注网易云信博客 ...

January 21, 2019 · 2 min · jiezi