莫名其妙的大四下学期依然各种忙的不可开交,从上一次写到现在已经过去个把月了,最近更是因为其他的一些东西即将彻底霸占我的时间,考虑到以后可能没有时间再写这个项目了,决定结贴,本来还想拍个视频什么的,看来是顾不上了,如果以后突然很闲的话说不定会把视频和照片传上来。小车到此为止,已经不太可能花时间做的更完美了,我今天会写到最终的伪视频传输,比较遗憾的是由于个人手机ROM的原因GPS这个理论上最容易实现的功能没能写在程序中,诸位看客莫把我当标题党用板砖给拍了就行,好歹是实现了非常麻烦的2.x系统全兼容的伪视频传输(2.1传视频很烦的,往下看就知道了),诸位看客应该相信更期待这个而不是GPS吧?呵呵,反正GPS实现很简单,有兴趣的自己随便写一下就好了………………
牢骚结束,切入正题,这章开始就要使用手机的3G网络来传输数据了,我们要实现的是用另一台手机可以通过这台手机控制小车,那么,首先一个技术难点就来了,大家看一下自己手机的ip地址就知道,在目前国内的网络环境下……手机是在局域网中的……也就是说,两台手机基本没有方法可以通过ip地址直接建立连接,必须得通过一个公网的服务器 进行中转才可以。可是做到这一步就需要的成本太大了,毕竟不是每一个做同类型东西的哥哥们都可以找的来可以随便写程序的公网服务器,我们只好寻求别的方法来解决这个问题。
我们尝试过好多种号称可以穿透局域网网关的连接方法,各种JAVA环境下号称有足够大的用户群的P2P技术,但是既繁琐又足够不稳定,就没成功的连通过 =_= …………最终放弃了直接通过socket进行连接的方式。大家读过早期android入门书籍的应该都见过这么个东西"GTalkAPI",号称可以直接通过GTalk在手机之间发送INTENT。这可是个大好的功能啊,直接就可以把3G的传输透明化了。于是乎大家都兴冲冲的运行实例代码……然后发现在android正式发行之后这个库就因为安全性问题直接被删掉了…………大囧啊……但是GTalk真心是个好东西,用这个我们就可以像用QQ一样发送控制命令了,想把控制端做在其他平台上也就容易了,就是所谓的可移植性和可扩展性非常好。于是乎,曲线救国,我们自己找第三方的库来用,几经波折,发现这么一个关键词“XMPP协议”。
关于这个协议和它的相关内容就不做太多介绍了,有兴趣的可以自行搜索,需要注意的是所有传过来的数据都是XML的文本形式的,就是说传输二进制的附件会有问题,这个我们后边专门说一下。如此,我们注册两个GTalk账号在两端手机的程序上登录,就可以通过文字命令的方式传输数据了。据说android上用的xmpp库有问题所以普遍建议使用人们修改过的库来用(我没有验证真伪),于是在CSDN上搜到了一个非常不错的工程,以后的程序都是基于这个工程开发,感谢原作者,此处贴出该程序出处共诸看客自行下载,这一篇我也就不上传工程文件了,只贴出自己写的代码,到时候拷在那个工程里就好了。
传送门:http://download.csdn.net/detail/wqj824/3419215
如此,我们就可以用文字传输命令了,我们在原程序中做个钩子函数截取远端发来的消息读取其中的命令,于是乎,第二个技术难点:如何读取命令
为了区分无聊的聊天信息和控制命令,我们把控制命令规定为如下的格式:
#command action turnL 64 moveF 7 keep 200
#command表示接下来是命令内容
action表示这是一个动作命令
turnL表示动作中左转(或者是turnR),紧跟着的数字是0~100的整数表示转动的幅度百分数
moveF表示动作中前进(或者moveB),紧跟着的数字是0~100的整数表示速度的百分数
当然,我们还使用了其他命令来捕获对视频的控制,为了能给准确、高效的检测出命令和其中的参数,我们使用正则表达式来检测并提取参数(不了解的看客可以自学)。下面是核心代码:
// #command action turnL 64 moveF 7 keep 200
Pattern pattern = Pattern
.compile("^#command\\saction\\sturn([LR])\\s(\\d+)\\smove([FB])\\s(\\d+)\\skeep\\s(\\d+)");
Matcher matcher = pattern.matcher(input);
String LR = "";
String LRNum = "";
String FB = "";
String FBNum = "";
String Keep = "";
int LRInt = 0;
int FBInt = 0;
int KeepInt = 0;
double LRDouble = 0;
double FBDouble = 0;
while (matcher.find()) { // Find each match in turn
// Access a submatch group
LR = matcher.group(1);
LRNum = matcher.group(2);
FB = matcher.group(3);
FBNum = matcher.group(4);
Keep = matcher.group(5);
needReply = true;
isFound = true;
if (D)
Log.d(TAG, "+++ Found Action +++");
}
if (isFound) {
try // 尝试转换INT(之所以没有在正则表达式中判断越界是应为那样的代码过于难阅读)
{
LRInt = Integer.parseInt(LRNum);
FBInt = Integer.parseInt(FBNum);
KeepInt = Integer.parseInt(Keep);
} catch (Exception e) {
HookResult = "输入的数值不能转化为整型";
if (D)
Log.d(TAG, "+++ Not Integer +++");
e.printStackTrace();
}
// 判断int数值是否越界
if (LRInt < 0 || LRInt > 100 || FBInt < 0 || FBInt > 100
|| KeepInt < 0 || KeepInt > 1000) {
if (D)
Log.d(TAG, "+++ Out of bond +++");
HookResult = "输入的数值越界,请注意转向和速度值为0~100的整数,保持时间为0~1000的整数毫秒数";
} else {
if (LR.equals("L")) {
if (FB.equals("F")) {
if (D)
Log.d(TAG, "+++ LF +++");
LRDouble = LRInt * (-0.01);
FBDouble = FBInt * (0.01);
BroadContralCommand(LRDouble, FBDouble, KeepInt);
HookResult = "收到命令:左转" + LRInt + "%,速度" + FBInt
+ "%,前进" + KeepInt + "毫秒";
} else {
if (D)
Log.d(TAG, "+++ LB +++");
LRDouble = LRInt * (-0.01);
FBDouble = FBInt * (-0.01);
BroadContralCommand(LRDouble, FBDouble, KeepInt);
HookResult = "收到命令:左转" + LRInt + "%,速度" + FBInt
+ "%,后退" + KeepInt + "毫秒";
}
} else {
if (FB.equals("F")) {
if (D)
Log.d(TAG, "+++ RF +++");
LRDouble = LRInt * (0.01);
FBDouble = FBInt * (0.01);
BroadContralCommand(LRDouble, FBDouble, KeepInt);
HookResult = "收到命令:右转" + LRInt + "%,速度" + FBInt
+ "%,前进" + KeepInt + "毫秒";
} else {
if (D)
Log.d(TAG, "+++ RB +++");
LRDouble = LRInt * (0.01);
FBDouble = FBInt * (-0.01);
BroadContralCommand(LRDouble, FBDouble, KeepInt);
HookResult = "收到命令:右转" + LRInt + "%,速度" + FBInt
+ "%,后退" + KeepInt + "毫秒";
}
}
}
}
isFound = false; //加这句是为了防止下次的结果不分青红皂白覆盖前面的结果,删了试试就知道了
对于如何调用摄像头,主流的是有两种方法,一种是发送一个INTENT调用系统的拍照程序,这种方式返回一张照片的时间太久了,我们不使用,另一种是调用Camera类在程序中完成拍照,这种速度很快,但是会出现非常多的问题,很不幸,我们为了实现连续的拍照必须使用这种方法……
另外要实现伪视频,用拍照的方式还是速度达不到要求,为更快的获得图像我们截取预览图的截图来快速获得当前的图像。这是最麻烦的地方,我们等下引用别人的文章来讲一下,先贴上摄像头调用的关键代码:(同样是个人手机的原因,一些代码被注释掉了,大家可以适当的启用)
/*
* function: 非preview时:实例化Camera,开始preview 非preview时and相机打开时:再设置一次preview
* preview时:不动作
*/
private void initCamera() throws IOException {
if (!bPreviewing) {
/* 若相机非在预览模式,则打开相机 */
camera = Camera.open();
}
// 非预览时and相机打开时,开启preview
if (camera != null && !bPreviewing) {
Log.i(TAG, "inside the camera");
/* 创建Camera.Parameters对象 */
Camera.Parameters parameters = camera.getParameters();
/* 设置相片格式为JPEG */
// parameters.setPictureFormat(PixelFormat.JPEG);
/* 指定preview的屏幕大小 */
// parameters.setPreviewSize(intScreenWidth, intScreenHeight);
/* 设置图片分辨率大小 (不能用) */
// parameters.setPictureSize(intScreenWidth, intScreenHeight);
/* 将Camera.Parameters设置予Camera */
camera.setParameters(parameters);
/* setPreviewDisplay唯一的参数为SurfaceHolder */
camera.setPreviewDisplay(mSurfaceHolder);
camera.setPreviewCallback(this);// 设置预览帧的接口,就是通过这个接口,我们来获得预览帧的数据的
/* 立即运行Preview */
camera.startPreview();
bPreviewing = true;
}
}
/* func:停止preview,释放Camera对象 */
private void resetCamera() {
if (camera != null && bPreviewing) {
camera.stopPreview();
/* 释放Camera对象 */
camera.release();
camera = null;
bPreviewing = false;
}
}
private void takeAPicture() {
if (camera != null && bPreviewing) {
/* 调用takePicture()方法拍照 */
camera.takePicture(null, null, jpegCallback);// 调用PictureCallback
// interface的对象作为参数
}
}
private void takeAShotCut() {
if (camera != null && bPreviewing) {
/* 调用截图方法拍照方法拍照 */
// Bitmap newBitmap = mSurfaceView01.getDrawingCache();
/*
* Bitmap newBitmap = this.makebitmap(this); newBitmap =
* BitmapFactory.decodeByteArray(YUVData, 0, YUVData.length);
*/
byte[] YUVData = frameData.clone();
int imageWidth = camera.getParameters().getPreviewSize().width;
int imageHeight = camera.getParameters().getPreviewSize().height;
int RGBData[] = new int[imageWidth * imageHeight];
byte[] mYUVData = new byte[YUVData.length];
System.arraycopy(YUVData, 0, mYUVData, 0, YUVData.length);
decodeYUV420SP(RGBData, mYUVData, imageWidth, imageHeight);
Bitmap newBitmap = Bitmap.createBitmap(imageWidth, imageHeight,
Bitmap.Config.ARGB_8888);
newBitmap.setPixels(RGBData, 0, imageWidth, 0, 0, imageWidth,
imageHeight);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
newBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
byte[] jpegArray = stream.toByteArray();
/*
* 发送图片的相关代码
*/
String to = mRecipient.getText().toString();
String text = "ShotCut-JPEG";
Message msg = new Message(to, Message.Type.chat);
msg.setBody(text);
DefaultPacketExtension jpegExtension = new DefaultPacketExtension(
EElementName, ENameSpace);
jpegExtension.setValue(EValueName, Base64.encodeBytes(jpegArray));
jpegExtension.setValue(ETimeName, System.currentTimeMillis()+"");
msg.addExtension(jpegExtension);
if (D)
Log.d(TAG,
"+++ Jpeg Extended +++"
+ Base64.encodeBytes(jpegArray)
.substring(0, 40));
sendMessage(msg);
}
}
private PictureCallback jpegCallback = new PictureCallback() {
public void onPictureTaken(byte[] data, Camera camera) {
/*
* resetCamera(); try { initCamera(); } catch(Exception e) {
* Log.e(TAG, "initCamera Error after snapping"); }
*/
camera.startPreview();
/*
* 发送图片的相关代码
*/
String to = mRecipient.getText().toString();
String text = "Photo-JPEG";
Message msg = new Message(to, Message.Type.chat);
msg.setBody(text);
DefaultPacketExtension jpegExtension = new DefaultPacketExtension(
EElementName, ENameSpace);
jpegExtension.setValue(EValueName, Base64.encodeBytes(data));
jpegExtension.setValue(ETimeName, System.currentTimeMillis()+"");
msg.addExtension(jpegExtension);
if (D)
Log.d(TAG, "+++ Jpeg Extended +++"
+ Base64.encodeBytes(data).substring(0, 40));
sendMessage(msg);
}
};
对于2.2以后的系统,截图操作就相对简单多了(虽然还是很麻烦),但是对于我这样2.1的老手机老系统来说就麻烦的多了,理论上截屏只要在view中直接调取就可以了,但是偏偏android对于摄像头的预览是用surfaceview的,而且为了减小延迟几乎全部的中间内容都是C++写的,也就是说java怎么都截取不到图像,包括全局的截图,于是只好在onPreviewFrame这个回调函数中做文章,然而大家会发现这时候我们得到的数组是摄像头传回来的YUV信号,android并没有做解码的操作……………………于是乎就只好自己写YUV的解码了(不过在2.2以后就可以调用系统的解码了),由于大家普遍对这个解码不懂,所以这个函数大家普遍看不懂…………在运行中也偶尔出现点错误但是影响不大,还有就是得到的图像是躺倒的,接收图片要注意旋转。下面贴上引用的文章地址和核心代码:
传送门:http://eyehere.net/2011/android-camera-2/
//缓冲图像帧用
private byte[] frameData;
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
frameData = data.clone();
// get the prew frame here,the data of default is YUV420_SP
// you should change YUV420_SP to YUV420_P
}
/**
* 转换YUV420SP到rgb的代码
*
* @param rgb
* @param yuv420sp
* @param width
* @param height
*/
static public void decodeYUV420SP(int[] rgb, byte[] yuv420sp, int width,
int height) {
final int frameSize = width * height;
for (int j = 0, yp = 0; j < height; j++) {
int uvp = frameSize + (j >> 1) * width, u = 0, v = 0;
for (int i = 0; i < width; i++, yp++) {
int y = (0xff & ((int) yuv420sp[yp])) - 16;
if (y < 0)
y = 0;
if ((i & 1) == 0) {
v = (0xff & yuv420sp[uvp++]) - 128;
u = (0xff & yuv420sp[uvp++]) - 128;
}
int y1192 = 1192 * y;
int r = (y1192 + 1634 * v);
int g = (y1192 - 833 * v - 400 * u);
int b = (y1192 + 2066 * u);
if (r < 0)
r = 0;
else if (r > 262143)
r = 262143;
if (g < 0)
g = 0;
else if (g > 262143)
g = 262143;
if (b < 0)
b = 0;
else if (b > 262143)
b = 262143;
rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000)
| ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
}
}
}
接下来还有第三个技术难点,就是XMPP的附件发送,由于XMPP在传输时是使用XML文本来传输的,如果直接把2进制代码写在里面的话会出现格式错误(比如提前识别个尖括号出来这类的)所以需要对2进制文件进行相应的编解码才能传输,手机上由于运算能力的限制,习惯上使用base64编码把2进制流编码成字符流,有兴趣的可以查一下base64编码规则,这里要说的是jdk自带的base64编码方法不能用,android会提示不能使用而无法编译………………很不幸,貌似又得自己写base64编解码了…………不过好在这次的XMPP库中自带了base64编解码的相关方法,找到以后直接调用就好,但是这个程序会偶尔报错,初步怀疑是这个编解码有格式问题,但是调试太不方便了,就没能查出具体原因,大家多个心眼,可以在调试的时候注意一下这里。核心代码就在刚才的方法里面……
下一篇文章贴出完整程序