【视频采集方案】

Android视频采集,传输,编码解码的方案总结
  1.  ipcamera-for-android

服务器 :    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类采集视频流。

2 .  ivideochat

服务器 :   通过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协议半公开,难度比较大

3. Camera  and  Socket

通过Camera拍摄预览中设置setPreviewCallback实现onPreviewFrame接口,实时截取每一帧视频流数据 .  把一帧一帧的图像压缩通过socket发送到服务器,服务器可以直接观看。而要想让另一台手机也能观看,可以让服务器转发来实现。

服务器 :    Android手机通过camera类拍摄视频,把一帧一帧的图像压缩通过socket发 送到服务器,服务器可以直接观看

客户端 : 要想让另一台手机也能观看,可以让服务器转发来实现。

例子网上有很多,   http://download.csdn.net/detail/xiaoliouc/4933610

缺点:  通过一帧一帧的发送数据,传输过程耗费大量流量。玩玩可以,但实际项目 中不可取。

4.  流媒体服务器方式:ffmpeg或Getstreamer等获取Camera视频 

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协议等。使用软编码,效率比较低,且视频质量较差。

5. MediaRecorder+h264

通过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的视频流数据。

6. spydroid -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服务器的步骤:

1、Add this to your manifest:

 android:name="net.majorkernelpanic.streaming.rtsp.RtspServer"/>

把RtspServer这个服务在androidManifest文件中进行注册。在libstreaming库中,rtsp服务器是作为service组建实现的,这与spydroid的实现方式完全不一样。

2、You can change the port used by the RtspServer:

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==; 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打包处理,发送。

 

写完后才发现排版极为糟糕,重新整理一下

 

spydroid源码分析(三):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个字节

  1. buffer[0] = (byte) Integer.parseInt("10000000",2);

M:标记一些重要的事件(由应用程序定义);  占1个bit

PT:净荷数据类型; Payload Type,占7个bit

  1. java代码表示,buffer[1]表示rtp包的第1个字节
  1. buffer[1] = (byte96;

这里的96的数值是你自己规定的,在上一篇文章可以看到我们建立rtsp连接的时候,describe描述需要传输Payload Type,这里的数值需要和那个值相同,上文可以看到我用的96. 如果你不使用rtsp协议,直接通过udp的方式发送到对方VLC,你需要在sdp文件指定其数值为96、

 

SN:序列号,每个分组的序列号(初始值随机),用来检测分组的丢失并恢复分组的序列;

在rtp包中占第2,3这2个字节。

java代码表示,

  1. private void updateSequence() {
  2.         setLong(++seq, 24);
  3.     }
  4. private void setLong(long n, int begin, int end) {
  5.         for (end--; end >= begin; end--) {
  6.             buffer[end] = (byte) (n % 256);
  7.             n >>= 8;
  8.         }
  9.     }

随着传递一个packet,sequence的数值加1.

 

TS:时间戳,反映RTP净荷中的第一个采样数据的采样时间。时间的粒度是净荷类型相关的。

在rtp包中,占4,5,6,7这4个字节。

java代码如下

  1. public void updateTimestamp(long timestamp) {
  2.         setLong(timestamp, 48);
  3.     }
  4.     private void setLong(long n, int begin, int end) {
  5.         for (end--; end >= begin; end--) {
  6.             buffer[end] = (byte) (n % 256);
  7.             n >>= 8;
  8.         }
  9.     }

timestimp在spydroid源代码中是通过请求传输每个包的平均值,然后经过一定的计算得到的。

 

SSRC:同步源标识符,用于标识同步源。同步源指的是,例如,一段影片的音频和视频通过不同的RTP流传输,它们是同步的。每个同步源是负责发送RTP分组并在RTP中设置序列号和时间戳的实体。

在rtp包中,占8,9,10,11这4个字节。

java代码如下

  1.     setLong((ssrc=(new Random()).nextInt()),8,12);
  2. public void setSSRC(int ssrc) {
  3.         this.ssrc = ssrc;
  4.         setLong(ssrc,8,12);
  5.     }
  6.     public int getSSRC() {
  7.         return ssrc;
  8.     }
  9. private void setLong(long n, int begin, int end) {
  10.         for (end--; end >= begin; end--) {
  11.             buffer[end] = (byte) (n % 256);
  12.             n >>= 8;
  13.         }
  14.     }

在上面的代码中,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源代码的时候添加代理服务器参数,并设置代理服务器。这个只是官方的说法,能不能真正解决没有尝试过。

 

 

手机Android音视频采集与直播推送,实现单兵、移动监控类应用

恰逢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直接粘贴帧图

你可能感兴趣的:(【视频采集方案】)