服务器 : Android手机充当服务器,使用NanoHTTPD充当服务器
客户端 : 手机或者pc输入http://server ip:8080观看。
这种方案可以参考 ipcamera-for-android开源项目,网址 http://code.google.com/p/ipcamera-for-android/source/checkout
可以运行的源代码可以在这下载 http://download.csdn.net/detail/xiaoliouc/4933558
缺点:android手机必须支持MP4+ARM_BN格式,有些手机不兼容,延时有点厉害,而且用到NDK编程,现在看来方案不可取
现在似乎有个更新的版本,项目名字:android-eye-master
需要的可以百度或者留言, 不过还是用camera类采集视频流。
服务器 : 通过rtmp协议发布服务到red5服务器,可用red5自带的的OFLA Demo测试.
客户端 : Android手机采用juv-rtmp-client.jar包,网上有破解的收费包。
播放端 : 使用支持rtmp协议的播放器播放,如ffplay,vlc,ffmpeg等.
服务器当然是red5了,可以用red5自带的的OFLA Demo做测试.
客户端 ivideochat 下载地址 : http://download.csdn.net/detail/xiaoliouc/4933594
缺点:demo延迟很厉害,仅供参考。rtmp协议半公开,难度比较大
通过Camera拍摄预览中设置setPreviewCallback实现onPreviewFrame接口,实时截取每一帧视频流数据 . 把一帧一帧的图像压缩通过socket发送到服务器,服务器可以直接观看。而要想让另一台手机也能观看,可以让服务器转发来实现。
服务器 : Android手机通过camera类拍摄视频,把一帧一帧的图像压缩通过socket发 送到服务器,服务器可以直接观看
客户端 : 要想让另一台手机也能观看,可以让服务器转发来实现。
例子网上有很多, http://download.csdn.net/detail/xiaoliouc/4933610
缺点: 通过一帧一帧的发送数据,传输过程耗费大量流量。玩玩可以,但实际项目 中不可取。
android手机通过camera类拍摄视频,把拍摄的视频通过h264编码,可以采用软编码(使用x264库或者opencore软件库),java类通过jni调用编译后的so文件来实现。然后通过基于udp的rtp协议传输到服务器。为什么不使用tcp协议呢,因为tcp的重传机制会产生延时和抖动,而单独使用udp传输协议本身是面向无连接的,不能提供质量保证,需要在udp协议只上采用rtp或者rtcp提供流量控制和拥塞控制服务。服务器通过ffmpeg对接收的h264解码并播放。播放可以使用VLC media player。如果对c++比较熟悉,可以看看live555这个开源项目。
缺点:需要懂得的知识很多,jni啊,h264编码解码 ,rtp协议等。使用软编码,效率比较低,且视频质量较差。
通过Andoriod的MediaRecorder,在SetoutputFile函数中绑定LocalSocket实现 .
android手机通过mediaRecorder类拍摄视频,其中当然包括音频了。把拍摄的视频通过h264编码,可以采用硬编码(面向手机的硬件直接操作),只能针对3gp,mp4视频格式。
方法参考 http://blog.csdn.net/zblue78/article/details/6078040
这篇博客,里面讲的很详细,提取h264的sps,pps,可以参考
http://blog.csdn.net/peijiangping1989/article/details/6934317
winHex是一款好用的16进制查看工具,下载地址 http://download.csdn.net/detail/xiaoliouc/4928773
代码网上有很多,个人理解是:mediaRecorder录制视频(3gp,MP4),可以通过mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());预览视频,通过localsocket发送到本地的localserversocket的h264实时视频流。
这个过程涉及到硬编码,硬编码个人理解是,在预览过程或者提前确定视频的sps,pps,head(一般为0x00000001),不同的手机硬件不一样。把得到的这些参数写入h264,得到正确的h264视频流,然后把流推送到流媒体服务器,使用支持rtsp协议的播放器播放,比如vlc.
6、采用HLS协议,服务器采用nginx,ffmpeg解码。nginx服务器搭建过程,ffmpeg安装过程 见我前几篇文章。
然后用ffmpeg对解码后的mp4文件进行ts切片,生成带有索引的m3u8文件,然后客户端就可以通过浏览器http://ip :port/ *.m3u8访问。
过程貌似是这样的,但自己由于刚接触不到一个周,还不太理解。
7. smartcam的一个开源项目,看了下源代码,发现其实现原理是利用Android 的camera图像的预采集,通过实现PreviewCallback类的回调函数onPreviewFrame,获得camera采集的原始图像数据之后,压成jpeg格式传到pc端。pc端对接收到的jpeg图像序列进行实时解压和显示,就达到了预想的效果。
虽然这种方式稍微显得比较笨拙,这个方式还可以接受。但是不可接受的是jpeg只是帧内压缩,320x280的图片序列,FPS大概是10上下,网络流量就到达了100kb/s以上。这个几乎是无法实际应用的。
于是必须直接传视频流,MPEG4或者H.264格式。貌似我的开发机上(HTC G8)只支持到MPEG4,所以还是选取MPEG4。但是如何实时采集视频流是一个大问题,毕竟在video方面,Android并没有提供一个类似于OnPreviewFrame的回调函数。
想到用opencore或者更为新一点的stagefright,大概看看了其sdk的框架后,马上泄气了,这个太庞大了。在http://www.linuxidc.com/Linux/2011-04/34468.htm的帖子中提到一个很好的解决方案,就是利用MediaRecorder:MediaRecorder的输出路径(其实叫file descriptor)除了是本地文件路径之外,还可以绑定socket端口。也就是说,通过一个socket端口,就可以实时获得MediaRecorder的视频流数据。
相对容易、且效果不错的方法,android手机上搭建rtsp服务器,另一台手机使用 VLC播放器输入rtsp://ip:port/播放视频。具体原理是,通过android手机对mediaRecorder录制视频,把localsocket传输到本地的流经过硬编码,添加rtp头,分离NALU包,根据rtsp协议交互过程把数据发送到对方。
代码参考spydroid了,源代码可以通过svncheckout,能够正常运行,且效果不错。
http://code.google.com/p/spydroid-ipcamera/source/checkout 。
网上还没有分析spydroid源码的文章,等自己空了有机会分析下源代码。
7. 前面讲的都是单向视频,如果是双向视频,其实就是视频会议了,可以参考sipdroid开源代码了
网址 : http://code.google.com/p/sipdroid/source/checkout
用Libstreaming打造属于自己的RTSP服务器
Libstreaming是一个开源的流媒体框架,它可以让手机变成一台流媒体服务器,直接在PC端查看手机摄像头的实时画面。值得一提的是它的作者也是spydroid的作者。按照作者的说法,spydroid是利用该库完成流媒体传输的,但据笔者的分析观察,此说法并不十分确切。Libstreaming是spydroid的抽象与升华,RTSP服务器的实现方式也有很大的不同。
巧妇难为无米之炊,我们先把Libstreaming的源代码下载下来。地址:https://github.com/fyhertz/libstreaming 下载完毕后导入eclipse,并新建工程引用该库。这里要颇为注意,新建的工程必须和Libstreaming在同一个盘符下,否则可能出现引用失败的问题。
接下来看看官方文档中给出的创建RTSP服务器的步骤:
android:name="net.majorkernelpanic.streaming.rtsp.RtspServer"/>
把RtspServer这个服务在androidManifest文件中进行注册。在libstreaming库中,rtsp服务器是作为service组建实现的,这与spydroid的实现方式完全不一样。
Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
editor.putString(RtspServer.KEY_PORT, String.valueOf(1234));
editor.commit();
The port is indeed stored as a String in the preferences, there is a good reason to that. The EditTextPreference object saves its input as a String and cannot easily (one would need to override it) be configured to store it as an Integer.
可以改变rtsp服务器的端口,当然,这是非必需的。默认端口是8086。若想改变端口,必须通过sharedPreference完成。先获取一个指向本activity的sharedPreference的editor对象,再将指定的端口号put进去。至于为什么用String类型而不是用整形存储端口号,主要是考虑到EditTextPreference对象的保存类型是string。
3、Configure its behavior with the SessionBuilder:
SessionBuilder.getInstance()
.setSurfaceHolder(mSurfaceView.getHolder())
.setContext(getApplicationContext())
.setAudioEncoder(SessionBuilder.AUDIO_AAC)
.setVideoEncoder(SessionBuilder.VIDEO_H264);
sessionBuilder是session的建造者。而session又是服务器与客户端间通信的载体。此部分主要是设置sessionBuilder的一些选项,如音频编码器、视频编码器等等。注意,sessionBuilder用到了单例设计模式,整个程序共享这一个sessionBuilder对象。此外,值得一提的是sessionBuilder的setSurfaceHolder方法,此方法其实没有太大的用处,不过因为android 某些API的限制,使得如果你要录制视频,必须要有有效的surface。
5、Start and stop the server like this:
// Starts the RTSP server
context.startService(new Intent(this,RtspServer.class));
// Stops the RTSP server
context.stopService(new Intent(this,RtspServer.class));
最后,启动或停止service。
完整的示例:
1、布局文件——activity_main.xml
2、代码——MainActivity.java
package com.dyc.spydroidrtspserver;
import net.majorkernelpanic.streaming.SessionBuilder;
import net.majorkernelpanic.streaming.rtsp.RtspServer;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences.Editor;
import android.view.Menu;
import android.view.SurfaceView;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
SurfaceView surfaceView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);//加载布局
surfaceView=(SurfaceView)findViewById(R.id.surface);
Editor editor=PreferenceManager.getDefaultSharedPreferences(this).edit();//获取sharedPreference的editor对象
editor.putString(RtspServer.KEY_PORT, "1234");//将新的端口号put进去
editor.commit();//提交更改
SessionBuilder.getInstance()
.setAudioEncoder(SessionBuilder.AUDIO_AMRNB)
.setVideoEncoder(SessionBuilder.VIDEO_H264).
setContext(getApplicationContext()).
setSurfaceHolder(surfaceView.getHolder());//配置sessionBuilder对象
this.startService(new Intent(this, RtspServer.class));//启动服务
displayIpAddress();//显示地址
}
private void displayIpAddress() {
WifiManager wifiManager=(WifiManager)getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo=wifiManager.getConnectionInfo();
int address=wifiInfo.getIpAddress();//获取IP地址,注意获取的结果是整数
Toast.makeText(this, "rtsp://"+intToIp(address)+":1234", Toast.LENGTH_LONG).show();//用toast打印地址
}
private String intToIp(int i) {//整形转IP
return (i & 0xFF)+ "." + ((i >> 8 ) & 0xFF)+ "." + ((i >> 16 ) & 0xFF) +"."+((i >> 24 ) & 0xFF );
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
3、androidManifest文件
我这里只是一个简单的演示 ,所以没有判断到底有没有用wifi。它官方有给示例,详见:https://github.com/fyhertz/libstreaming-examples 可以去看一下
spydroid源码分析(一):介绍spydroid每个包的大体功能
看了接近一周的spydroid源代码,对spydroid这个开源项目有了一定的认识。也许有些理解不一定正确,给后来者一点启示。也是自己对rtsp协议,rtp协议的总结。
在windows下,如果安装了svn,可以通过 svn checkout http://spydroid-ipcamera.googlecode.com/svn/ 下载源码,最新源码是spydroid6.7.1
下载后,导入eclipse工程,直接就可以编译运行。
在这里只关心其src源码部分,其他地方都比较简单,就不介绍了。src源码有以下几个package。
net.majorkernelpanic.http主要是介绍http server,spydroid自身内置http服务器,客户端可以通过在VLC等播放器中输入http://ip:8080/播放
net.majorkernelpanic.mp4主要是介绍提取mp4文件的profile,sps,pps等信息
net.majorkernelpanic.networking 主要是介绍rtsp服务器部分,spydroid自身内置rtsp服务器,客户端可以通过在VLC等播放器中输入rtsp://ip:8086/播放
net.majorkernelpanic.rtp主要是介绍rtp协议通信
net.majorkernelpanic.spydroid主要是activity的界面部分。
net.majorkernelpanic.streaming主要是stream接口和抽象类
net.majorkernelpanic.streaming.audio介绍音频部分
net.majorkernelpanic.streaming.video介绍视频部分
以后针对mp4,networking ,rtp,stream这4个包的内容重点分析
未完待续
spydroid源码分析(二):spydroid运行流程
这几天空闲的时候在看《struts2技术内幕》这本书,作者downpour说的这句话我很赞同,忘了原文了, 学习开源项目,不是一个包一个包的阅读代码,而是通过动态运行项目,通过断点调试,来获取相关信息。 我也打算用这种方式来看spydroid源代码,但了解每个package大体的功能是必须的。
如果spydroid已经安装到了android手机上,开启这个软件,VLC就可以通过rtsp://手机的ip:8086/访问。在这里以H264来说明spydroid的运行流程,其他类似。在这里需要吧option的encode编码设为h264.调试android源代码,可以通过logcat打印相关信息
程序运行时,进入net.majorkernelpanic.spydroid.SpydroidActivity,该activity 运行时候,开启http server ,rtsp server。这里重点关注rtsp 服务。
进入 net.majorkernelpanic.networking.RtspServer
rtspserver开启后,启动一个线程RequestListenerThread,负责监听客户端(这里用VLC)的请求,public void start() throws IOException {
if (running) return;
running = true;
listenerThread = new RequestListenerThread(port,handler);
listenerThread.start();
}
当有客户端请求的时候,开启一个workerTread线程。一个线程session代表一个请求
new WorkerThread(server.accept(), handler).start();
VLC向rtsp服务器进行交互时,这里就需要用到rtsp协议的内容了,主要分为Options,Describe,Setup,play,teardown这5步骤。
下面是我进行rtsp连接,服务器与客户端请求与响应的详细信息,
//当来自192.168.1.26的VLC客户端向手机服务器发送rtsp://192.168.1.60:8086请求时
Connection from 192.168.1.26 //来自192.168.1.26的请求
//下面的C表示客户端client,S表示服务器server
C-S:OPTIONS rtsp://192.168.1.60:8086/ RTSP/1.0 //可用选项
S-C: Public: DESCRIBE,SETUP,TEARDOWN,PLAY,PAUSE //描述信息、建立连接、关闭、播放、暂停
C-S:DESCRIBE rtsp://192.168.1.60:8086/ RTSP/1.0
S-C:
v=0
o=- 1357627796453 1357627796453 IN IP4 192.168.1.60 //1357627796453是当前的timestamp信息,timestamp =System.currentTimeMillis();
s=Unnamed
i=N/A
c=IN IP4 192.168.1.26
t=0 0 //t=0 0意味着会话是永久的
a=recvonly
m=video 5006 RTP/AVP 96 //video指明是视频信息, 5006是指客户端VLC接收视频信息的udp端口号
b=RR:0
a=rtpmap:96 H264/90000 //这里的96的信息很关键,因为这个是rtp的负载类型,Payload type (PT): 7 bits。后面介绍rtp协议的时候会介绍,H264编码,90000是H264视频传输的默认视频采样频率,必须是这个值
a=fmtp:96 packetization-mode=1;profile-level-id=42c016;sprop-parameter-sets=ZOLAFukBQHsg,aM4G4g==; //packetization-mode=1指定rtp打包模式,有3种模式,数值只能为0,1,2,。0是单NAL单元模式 1是非交互模式 2是交互模式
profile,sps,pps这3个数值是从mp4中提取出来的base64编码,在net.majorkernelpanic.mp4这个包 有详细介绍。a=control:trackID=0 //trackID为0
Content-Base: 192.168.1.60:8086/
Content-Type: application/sdp //规定文件格式类型为sdp
C-S:SETUP 192.168.1.60:8086/trackID=0 RTSP/1.0
S-C:
Transport: RTP/AVP/UDP;unicast;destination=192.168.1.26;client_port=5006-5007;server_port=49749-49750;ssrc=431567f7;mode=play
Session: 1185d20035702ca //制定基于udp协议的rtp传输,目标地址,客户端端口、服务器端口,以及ssrc的数值,这里ssrc的数值很重要,它是同步源标识,synchronization source (SSRC) identifier,在rtp传输中,会包含这个内容
Cache-Control: no-cache
C-S: PLAY 192.168.1.60:8086/ RTSP/1.0
S-C:
RTP-Info: url=rtsp://192.168.1.60:8086/trackID=0;seq=0
Session: 1185d20035702ca //session标识
C-S: TEARDOWN 192.168.1.60:8086/ RTSP/1.0
在上面的内容中,可以看到options请求时,发送可用的状态。
describe请求时,发送流类型,在这里是h264视频流,以及mp4 的profile,sps,pps,在不同手机上,profile,sps,pps的数值不一定相同。这个是通过提取录制的该手机上的mp4文件的内容得到的。 除了H264,这里也可以是H263视频流,或者其他audio音频流。这里重点查看generateSessionDescriptor() 方法,比如在这里,选择H264,那么就可以看看H264Stream这个类的这个方法,看看它是如何获取profile ,sps,pps的setup请求时,主要关注stream.prepare(),stream.start()方法,prepare()的时候调用初始化视频录制的参数,比如H264编码,分辨率,帧数等相关信息。而start()方法就开始通过localsocket把录制的视频以流的形式发送到本地,而H264Packetizer通过获取其输入流,然后对其rtp打包处理,发送。
写完后才发现排版极为糟糕,重新整理一下
网上有很多的rtp协议介绍的文章,我也老生常谈的拿来使用了,
先介绍rtp包头,我们都知道,rtp包头占12个字节,1个字节byte当然是8个bit了,下面是详细介绍。看下面的这张图
V:版本号; Version(2),占2个bit,数值为2,二进制表示10
P:填充字段标识; Padding(0),占1个 bit,数值为0,二进制表示0
X:扩展头标识; Extension(0),占1个bit ,数值为0,二进制表示0
CSRC count(CC):贡献源数目,和后面的CSRC有关。CSRC,贡献源,指的是不同步的源。在网络中,可能会有混合器将来自不同地点的RTP流混合成一个RTP流以节省带宽, CSRC用来区分不同的源; Source Identifier(0),占4个bit,数值为0,二进制表示为0000
java代码表示,buffer[0]当然是指的是rtp包的第0个字节
M:标记一些重要的事件(由应用程序定义); 占1个bit
PT:净荷数据类型; Payload Type,占7个bit
这里的96的数值是你自己规定的,在上一篇文章可以看到我们建立rtsp连接的时候,describe描述需要传输Payload Type,这里的数值需要和那个值相同,上文可以看到我用的96. 如果你不使用rtsp协议,直接通过udp的方式发送到对方VLC,你需要在sdp文件指定其数值为96、
SN:序列号,每个分组的序列号(初始值随机),用来检测分组的丢失并恢复分组的序列;
在rtp包中占第2,3这2个字节。
java代码表示,
随着传递一个packet,sequence的数值加1.
TS:时间戳,反映RTP净荷中的第一个采样数据的采样时间。时间的粒度是净荷类型相关的。
在rtp包中,占4,5,6,7这4个字节。
java代码如下
timestimp在spydroid源代码中是通过请求传输每个包的平均值,然后经过一定的计算得到的。
SSRC:同步源标识符,用于标识同步源。同步源指的是,例如,一段影片的音频和视频通过不同的RTP流传输,它们是同步的。每个同步源是负责发送RTP分组并在RTP中设置序列号和时间戳的实体。
在rtp包中,占8,9,10,11这4个字节。
java代码如下
在上面的代码中,ssrc是产生的随机数,这个数值在建立rtsp连接的时候,SETUP这个步骤上需要用到 Integer.toHexString(ssrc);的数值,这个数值的的16进制是我们需要传递给VLC客户端的 ,你可以测试一下。如果不一致,那么发送到VLC客户端就无法正常播放
这不,rtp包头前12个字节就是这样,下面就是NALU的内容了
未完待续
一、H.264的RTP封包
感谢网友的讲解,非常详细 http://www.cppblog.com/czanyou/archive/2009/12/25/67940.html。在此做个记录,以备查询。
*********************************************
NALU header结构介绍
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
F: 1bit forbidden_zero_bit. h264规定这一位必须为0
NRI:2bit nal_ref_idc. 取00~11,代表NALU的重要性,如00的NALU解码器可以丢弃它而不影响图像的回放
Type:5bit nal_unit_type. 代表这个NALU单元的类型。
0 没有定义
1-23 NAL单元 单个NAL单元包
24 STAP-A 单一时间的组合包
25 STAP-B 单一时间的组合包
26 MTAP16 多个时间的组合包
27 MTAP24多个时间的组合包
28 FU-A 分片的单元
29 FU-B 分片的单元
nal_unit_type NAL类型
1 不分片、非IDR图像的片
2 片分区A
3 片分区B
4 片分区C
5 IDR图像中的片
6 补充增强信息单元(SEI)
7 序列参数集(SPS)
8 图像参数集(PPS)
9 分界符
10 序列结束
11 码流结束
12 填充
13-23 保留
**********************************************************
RTP header (一般12 bytes)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
V:版本号 (2 bits)
P:padding
M: marker bit(1 bit) 指示相同时间戳的最后一个
rtp包。
PT:payload type(7 bits) 96代表h264打包
SN: sequence number(16 bits) 包的序号,对于单个NALU模式与非交错打包方式,序号用于对定NALU解码顺序
Timestamp: 时间戳(32 bits) 设置为内容的采样时间戳。必须使用90kHz时钟频率。
**********************************************************
单一NALU模式
对于NALU的长度小于MTU大小的包,一般采用单一NAL单元模式。
一个原始的H.264 NALU常由 [Start Code] [NALU header] [NALU
Payload]三部分组成。
Start Code用于标示这是一个NALU单元的内容,必须是 “00 00 00 01” 或 “00 00 01”。
NALU header 仅一个字节,其后都是NALU内容。
RTP打包时,去除“00 00 00 01”或“00 00 01”的开始码,把其他数据封包到RTP包即可。
[RTP header] [NALU header] [NALU
payload]
实例:
有一个 h.264 的NALU是这样的:
[00 00 00 01 67 42 A0 1E 23 56 0E 2F ... ]
这是一个序列参数集NAL单元。[00 00 00 01]是四个字节的开始码,67是NALU header,42开头的数据时NALU内容。
封装成 RTP 包将如下:
[RTP header][67 42 A0 1E 23 56 0E 2F ... ] 去掉4个字节的开始码
************************************************************************
FU-A 分片模式 (Fragmentation Units)
当一个NALU长度大于MTU时,就必须对NALU进行分片封包。
[RTP header] [FU indicator] [FU header] [FU
payload]
FU indicator:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
NRI : 设置成NAL单元的NRI
Type: 28 表示 FU-A
FU header:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|R| Type |
+---------------+
S: 1 bit 设置成1,开始位指示NAL单元的开始。
E: 1 bit 设置成1,结束位指示分片NAL单元的结束。
R: 1 bit 预留位,必须设置为0
Type: 设置成NAL单元的荷载类型
***********************************************************************
RTP时间戳( http://blog.csdn.net/bripengandre/article/details/2238818 )
RTP包头的第2个32bit即为RTP包的时间戳,Time Stamp。
时间戳反映了RTP分组中的数组的第一个字节的采样时刻。在一次会话开始时的时间戳是随机选择的。即使没有信号发送时,时间戳的数值也要随时间不断的增加。接收端使用时间戳可准确知道应当在什么时间还原哪一个数据块,从而消除传输中的抖动。时间戳还可用来使音视频同步。
在RTP协议中并没有规定时间戳的粒度,这取决于有效载荷类型。因此RTP的时间戳又称为媒体时间戳,以强调这种时间戳的粒度取决于信号的类型。例如,对于8kHz采用的语音信号,若每隔20ms构成一个数据块,则一个数据块中包含有160个样本(0.02*8000=160)。因此每发送一个RTP分组,其时间戳的值就增加160.
关于libstreaming对时间戳的设置,有些许不解,有时间要好好研究。
二、RtpSocket类
未完待续...
8. android推送实时视频流到darwin流媒体服务器的思路
最近在弄android推送实时视频流到darwin流媒体服务器,现在的思路是在android端实现rtsp client,推送实时视频流到darwin,并在movies文件夹下生成sdp文件,vlc通过请求rtsp://darwin流媒体服务器ip:554/test.sdp的方式实现实时播放。
大致有3种思路:
第1种:移植live555到android上,借助live555里的代码:从本地读取mpg文件推送到darwin,改成从实时流推送到darwin服务器,大致思路是派生一个h264videoSource,从android录制获取流,通过pipe管道发送流,把该流推送到darwin。网上有个https://github.com/boltonli/ohbee/tree/master/android/streamer这个例子,但自己没测试通过,可以借鉴其实现思路。live555个人建议移植新版,可以参考文章 http://blog.csdn.net/baby313/article/details/7289489
第2种:网上有个rtspcamera的开源项目,源代码地址 :https://github.com/spex66/RTSP-Camera-for-Android ,可以学学该代码。个人推荐的是 由代码引申的这个帖子 https://github.com/spex66/RTSP-Camera-for-Android/issues/1 ,里面有人成功过实现把h264流推送到darwin流媒体服务器。个人尝试发现,这个代码也存在一些问题,表现在sdp文件生成的格式不对,通过修改代码SDP文件格式的形式,能出现效果,但延时很严重。
第3种:就是我以前研究的spydroid开源项目了,项目主页为http://code.google.com/p/spydroid-ipcamera/,可以在此https://github.com/fyhertz/spydroid-ipcamera下载最新源代码。我推荐的是这篇帖子http://code.google.com/p/spydroid-ipcamera/issues/detail?id=49&q=dss ,据说可以把h264实时流推送到darwin,但自己测试没通过。
其实第2种和第3种帖子的作者都是同一个,思路很好,可以供自己借鉴学习。
这段时间我才发现,使用rtsp协议实现音视频多路上传多路观看也有不错的解决方案,可能是我当初思维短路了,一直纠结服务器端口怎么处理的问题上,现在想想这不是问题。android端:实现rtsp client,以sdp文件名为标识,区分不同的流,把该流推送到darwin流媒体服务器,darwin流媒体服务器实现转播,并把接收的流通过ffmpeg转存为文件,其实可以参考vlc保存串流为文件的做法,vlc就可以通过请求darwin流媒体服务器实现多路观看的效果。
这些只是自己的一些初步思路,能不能行得通还待验证。
后记:在android推送实时视频流到darwin流媒体服务器 的过程中,需要解决防火墙的问题,毕竟在企业应用中,防火墙肯定说开启的。越过防火墙,按照官方的说法,在编译darwin源代码的时候添加代理服务器参数,并设置代理服务器。这个只是官方的说法,能不能真正解决没有尝试过。
恰逢2014 Google I/O大会,不难看出安卓在Google的推进以及本身的开放性作用下,已经快延生到生活的各个方面了,从安卓智能手机、平板,到可穿戴的Android Ware、眼镜、手表、再到Android汽车、智能家居、电视,甚至最近看新闻,日本出的几款机器人都是Android系统的,再把目光放回监控行业,传统监控中的移动终端设备,例如:单兵设备、手持设备、车载终端设备,包括家庭监控中用到的智能设备,都可以用Android系统替代了,不仅开发容易,而且易扩展,设备也更加智能了。
图 - Android在手持设备中的应用
一步一步来,我们先实现Android手机的音视频采集与上传的实时监控功能。Google Code上有一个开源项目:spydroid-ipcamera,spydroid能在Android手机中建立一个精简的HTTP Server和RTSP Server,功能类似于一般的IpCamera,既能够通过网页访问摄像机并修改监控配置,还能通过http或者rtsp协议,获取监控的实时音视频,而且从其代码结构中,可以看出,spydroid已经实现了RTSPServer、RTSPClient、RTP、RTCP、H264、AAC...等等功能,总之,咱们需要的Utility都已经具备了,只要将这些功能组合到一块就能实现咱们要的直播需求了。
图 - Android实现IPCamera的功能
在之前的博客“基于Darwin实现的分布式流媒体直播服务器系统”中,我们用Windows的摄像头和麦克风分别采集音视频数据,编码成H264和AAC,再用RTP打包,推送到Darwin流媒体服务器,实现直播。同样,我们参考EasyIpCamera的设计方法,App启动后,连接并保活至中心管理服务器,接收来自CMS的控制命令。采集安卓摄像头视频和mic声音,进行H264和AAC编码(这里我们只参考spydroid实现了硬编码,软编码在后续中将会更新进来,不过经过尝试,目前大部分Android音视频采集都支持硬编码),再通过RTSP和RTP,将实时音视频数据推送到流媒体服务器,并由流媒体服务器进行转发和分发,实现直播。
图 - Android接入EasyDSS
这里主要就是RTSP/RTP的推送过程,这个在之前的博客“基于DSS的先侦听后推送式流媒体转发”中详细描述了这个过程,具体的推送代码也可以参考live555的DarwinInjector类实现,咱们这里直接修改spydroid中的RTSPClient就可以实现ANNOUNCE/SETUP/PLAY/RTP过程了,具体流程源代码及协议流程也可以参考“用Darwin和live555实现的直播框架”中的描述。
Android采集端下载:http://pan.baidu.com/s/1kTwrasB EasyDSS(Win)服务器下载:http://pan.baidu.com/s/1ntoFSSh EasyDSS(Linux)服务器下载:http://pan.baidu.com/s/1c0b6bUo
使用方法与博客“基于Darwin实现的分布式流媒体直播服务器系统”中EasyIpCamera的方法一致,只需要配置EasyDSS服务器CMS的地址和端口号,就可以接入到EasyDSS,后续所有流程与EasyIpCamera类似,只有当有客户端请求实时视频的时候才会推送音视频流,其他情况只与CMS连接保活,流媒体处于Idle状态。
图 - Android采集端配置界面
实时效果:经过测试对吧,延时性与网络和手机的相关性很大,不同网络条件,不同手机硬件,可能对比出的延时效果不一样,用三星Glaxy3手机,在网络情况比较好的情况下,实时音视频的延时大概在1.3s(公网传输)左右:
图 - 连续测试2'52',延时1.35s
图 - 连续测试9'17'',延时1.58s
未来还会继续扩展和优化Android实时音视频采集程序,包括加入录像、抓图、软编码、对讲、抖动控制、录像上传等等方面,欢迎大家共同交流和进步!
用live555将内网摄像机视频推送到外网服务器,附源码
最近很多人问,如何将内网的摄像机流媒体数据发布到公网,如果用公网与局域网间的端口映射方式太过麻烦,一个摄像机要做一组映射,而且不是每一个局域网都是有固定ip地址,即使外网主机配置好了每一个摄像机的映射地址,也有可能会因为宽带公网ip地址变动而导致配置无效。
再换一个应用场景,当我们的所有IP摄像机都部署在一个没有有线网络的环境里面,所有的流媒体数据都要通过3G/4G网络发布出去。那么就必须有这么一个服务单元,能够通过先拉后推的方式,将内网的流媒体数据,推送并发布到外网的流媒体服务器上去:
在实现先拉后推式转发之前,我们先熟悉下live555的运转模式,live555主要运转的是一个source与sink的循环,sink想要数据,就调用source的getNextFrame,source获取到数据后,再调用afterGettingFrame回调,返回给sink数据,sink处理完后,再调用source的getNextFrame,如此循环。那么我们这里要实现从摄像机获取数据,那么我们的source就是一个RTPSource,我们又需要将数据以RTP的方式发送给流媒体服务器,那么我们的sink就是一个RTPSink,我们需要打通的就是一个RTPSource到一个RTPSink的过程。
ok,live555已经帮我们实现了大部分的功能,我们只需要将已有的部分组合起来就行了,这里我们主要用到的就是live555的ProxyServerMediaSession类和DarwinInjector类,我们用ProxyServerMediaSession从摄像机获取流媒体,再用DarwinInjector推送到Darwin Streaming Server,主要实现流程在下面代码注释中:
/*
功能描述: 一个简单的RTSP/RTP对接功能,从RTSP源通过基本的RTSPClient流程,获取到RTP流媒体数据
再通过标准RTSP推送过程(ANNOUNCE/SETUP/PLAY),将获取到RTP数据推送给Darwin流媒体
分发服务器。
此Demo只演示了单个源的转换、推送功能!
Author: [email protected]
时间: 2014/06/25
*/
#include "liveMedia.hh"
#include "BasicUsageEnvironment.hh"
#include "RTSPCommon.hh"
char* server = "www.easydss.com";//RTSP流媒体转发服务器地址,<请修改为自己搭建的流媒体服务器地址>
int port = 8554; //RTSP流媒体转发服务器端口,<请修改为自己搭建的流媒体服务器端口>
char* streamName = "live.sdp"; //流名称,推送到Darwin的流名称必须以.sdp结尾
char* src = "rtsp://218.204.223.237:554/live/1/66251FC11353191F/e7ooqwcfbqjoo80j.sdp";//源端URL
UsageEnvironment* env = NULL; //live555 global environment
TaskScheduler* scheduler = NULL;
char eventLoopWatchVariable = 0;
DarwinInjector* injector = NULL; //DarwinInjector
FramedSource* vSource = NULL; //Video Source
FramedSource* aSource = NULL; //Audio Source
RTPSink* vSink = NULL; //Video Sink
RTPSink* aSink = NULL; //Audio Sink
Groupsock* rtpGroupsockVideo = NULL;//Video Socket
Groupsock* rtpGroupsockAudio = NULL;//Audio Socket
ProxyServerMediaSession* sms = NULL;//proxy session
// 流转发过程
bool RedirectStream(char const* ip, unsigned port);
// 流转发结束后处理回调
void afterPlaying(void* clientData);
// 实现等待功能
void sleep(void* clientSession)
{
char* var = (char*)clientSession;
*var = ~0;
}
// Main
int main(int argc, char** argv)
{
// 初始化基本的live555环境
scheduler = BasicTaskScheduler::createNew();
env = BasicUsageEnvironment::createNew(*scheduler);
// 新建转发SESSION
sms = ProxyServerMediaSession::createNew(*env, NULL, src);
// 循环等待转接程序与源端连接成功
while(sms->numSubsessions() <= 0 )
{
char fWatchVariable = 0;
env->taskScheduler().scheduleDelayedTask(2*1000000,(TaskFunc*)sleep,&fWatchVariable);
env->taskScheduler().doEventLoop(&fWatchVariable);
}
// 开始转发流程
RedirectStream(server, port);
env->taskScheduler().doEventLoop(&eventLoopWatchVariable);
return 0;
}
// 推送视频到流媒体服务器
bool RedirectStream(char const* ip, unsigned port)
{
// 转发SESSION必须保证存在
if( sms == NULL) return false;
// 判断sms是否已经连接上源端
if( sms->numSubsessions() <= 0 )
{
*env << "sms numSubsessions() == 0\n";
return false;
}
// DarwinInjector主要用于向Darwin推送RTSP/RTP数据
injector = DarwinInjector::createNew(*env);
struct in_addr dummyDestAddress;
dummyDestAddress.s_addr = 0;
rtpGroupsockVideo = new Groupsock(*env, dummyDestAddress, 0, 0);
struct in_addr dummyDestAddressAudio;
dummyDestAddressAudio.s_addr = 0;
rtpGroupsockAudio = new Groupsock(*env, dummyDestAddressAudio, 0, 0);
ServerMediaSubsession* subsession = NULL;
ServerMediaSubsessionIterator iter(*sms);
while ((subsession = iter.next()) != NULL)
{
ProxyServerMediaSubsession* proxySubsession = (ProxyServerMediaSubsession*)subsession;
unsigned streamBitrate;
FramedSource* source = proxySubsession->createNewStreamSource(1, streamBitrate);
if (strcmp(proxySubsession->mediumName(), "video") == 0)
{
// 用ProxyServerMediaSubsession建立Video的RTPSource
vSource = source;
unsigned char rtpPayloadType = proxySubsession->rtpPayloadFormat();
// 建立Video的RTPSink
vSink = proxySubsession->createNewRTPSink(rtpGroupsockVideo,rtpPayloadType,source);
// 将Video的RTPSink赋值给DarwinInjector,推送视频RTP给Darwin
injector->addStream(vSink,NULL);
}
else
{
// 用ProxyServerMediaSubsession建立Audio的RTPSource
aSource = source;
unsigned char rtpPayloadType = proxySubsession->rtpPayloadFormat();
// 建立Audio的RTPSink
aSink = proxySubsession->createNewRTPSink(rtpGroupsockVideo,rtpPayloadType,source);
// 将Audio的RTPSink赋值给DarwinInjector,推送音频RTP给Darwin
injector->addStream(aSink,NULL);
}
}
// RTSP ANNOUNCE/SETUP/PLAY推送过程
if (!injector->setDestination(ip, streamName, "live555", "LIVE555", port))
{
*env << "injector->setDestination() failed: " << env->getResultMsg() << "\n";
return false;
}
// 开始转发视频RTP数据
if((vSink != NULL) && (vSource != NULL))
vSink->startPlaying(*vSource,afterPlaying,vSink);
// 开始转发音频RTP数据
if((aSink != NULL) && (aSource != NULL))
aSink->startPlaying(*aSource,afterPlaying,aSink);
*env << "\nBeginning to get camera video...\n";
return true;
}
// 停止推送,释放所有变量
void afterPlaying(void* clientData)
{
if( clientData == NULL ) return;
if(vSink != NULL)
vSink->stopPlaying();
if(aSink != NULL)
aSink->stopPlaying();
if(injector != NULL)
{
Medium::close(*env, injector->name());
injector == NULL;
}
ServerMediaSubsession* subsession = NULL;
ServerMediaSubsessionIterator iter(*sms);
while ((subsession = iter.next()) != NULL)
{
ProxyServerMediaSubsession* proxySubsession = (ProxyServerMediaSubsession*)subsession;
if (strcmp(proxySubsession->mediumName(), "video") == 0)
proxySubsession->closeStreamSource(vSource);
else
proxySubsession->closeStreamSource(aSource);
}
if(vSink != NULL)
Medium::close(vSink);
if(aSink != NULL)Medium::close(aSink);if(vSource != NULL)Medium::close(vSource);if(aSource != NULL)Medium::close(aSource);delete rtpGroupsockVideo;rtpGroupsockVideo = NULL;delete rtpGroupsockAudio;rtpGroupsockAudio = NULL;}
程序还有许多要完善的地方,只是一个简单的实现。
源码下载:
http://pan.baidu.com/s/1sj6Ue4l
非常感谢感谢6楼 Boris_Cao_2015 5天前 的回复,是这样的!
“按着这个代码不同时支持音视频,要修改LIVE555里面DarwinInjector源码, stream channel id记得加1,因为RTCP instance不存在,所以RTP流的stream channel id必须自动加1, 否则跟RTCP的stream channel id重合,这就是原因。跟楼主和大家分享。嘻嘻!”
方案一: 通过Android Camera拍摄预览中设置setPreviewCallback实现onPreviewFrame接口,实时截取每一帧视频流数据
http://www.cnblogs.com/skyseraph/archive/2012/03/26/2418665.html
方案二: 通过Android的MediaRecorder,在SetoutputFile函数中绑定LocalSocket实现
http://www.cnblogs.com/skyseraph/archive/2012/03/31/2427593.html
H264—MP4格式及在MP4文件中提取H264的SPS、PPS及码流http://www.cnblogs.com/skyseraph/archive/2012/04/01/2429384.html
方案三: 流媒体服务器方式,利用ffmpeg或GetStreamer等获取Camera视频
在Android中我所知道的音频编解码有两种方式:
(一)使用AudioRecord采集音频,用这种方式采集的是未经压缩的音频流;用AudioTrack播放实时音频流。用这两个类的话,如果需要对音频进行编解码,就需要自己移植编解码库了,比如可以移植ilbc,speex等开源编解码库。
ilbc的编解码实现可以查看这个专栏:http://blog.csdn.net/column/details/media.html
(二)使用MediaRecorder获取编码后的AMR音频,但由于MediaRecorder的特点,只能将流保存到文件中,但通过其他方式是可以获取到实时音频流的,这篇文章将介绍用LocalSocket的方法来实现;使用MediaPlayer来播放AMR音频流,但同样MediaPlayer也只能播放文件流,因此我用缓存的方式来播放音频。
以上两种方式各有利弊,使用方法(一)需移植编解码库,但可以播放实时音频流;使用方法(二)直接硬编硬解码效率高,但是需要对文件进行操作。
方案一: 不编码,直接通过Socket传输原始YUV420SP视频帧
方案二: JPEG. 将原始YUV420SP视频帧压缩转换为JPEG格式,JPEG传输
方案三: H.264/AVC.将原始YUV420SP视频帧压缩成H.264再传输
常见的基于H264的开源Encoder有JM、X264、T264、Hdot264等
http://blog.csdn.net/zblue78/article/details/6083374
Android 实时视频编码—H.264硬编码
http://www.cnblogs.com/skyseraph/archive/2012/04/04/2431771.html
方案四: MPEG4.将原始YUV420SP视频帧压缩成MPEG4再传输
方案一: Socket传输
方案二: HTTP传输
方案三: RTP/RTSP传输
方案四: 流媒体服务器方式,如live555等
与编码对应的的解码器
方案一: 通过Android VideoView
方案二: 通过Android MediaPlay
方案三: 通过Canvas直接粘贴帧图