网络摄像头Androi端显示(mjpeg)源码分析
main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:id="@+id/Tips" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/init_tips" android:textSize="40px" android:textColor="#00ff00" /> <Button android:id="@+id/btn_network" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/login" android:textSize="40px" android:textColor="#00ff00" /> <TextView android:id="@+id/statc001" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="40px" android:textColor="#00ff00" /> <Button android:id="@+id/btn_video" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/move" android:textSize="40px" /> </LinearLayout>
flash.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical" > <TextView android:id="@+id/hintTv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/login_hint" /> <EditText android:id="@+id/ip" android:hint="@string/ip_hint" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:text="192.168.0.10" /> <EditText android:id="@+id/port" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="8080" android:gravity="center" /> <Button android:id="@+id/connect" android:layout_width="fill_parent" android:layout_height="40.0dip" android:layout_marginLeft="10.0dip" android:layout_marginRight="10.0dip" android:layout_marginTop="20.0dip" android:text="@string/connect" android:textColor="#ffffffff" android:textSize="18.0sp" /> </LinearLayout>
mainactivity.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="视频显示"/> <com.mjpeg.view.MjpegView android:id="@+id/mjpegview" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </RelativeLayout>
strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="login">连接服务器</string> <string name="app_name">梧州学院图书馆刷卡入座系统</string> <string name="move">视频</string> <string name="init_tips">提示:请先打开WiFi或GPRS再连接网络</string> <string name="people1">空座</string> <string name="people2">有人</string> <string name="login_hint">connecting......;</string> <string name="ip">IP:</string> <string name="ip_hint">请输入IP地址</string> <string name="port">Port:</string> <string name="port_hint">端口1000到65535</string> <string name="connect">链接</string> <string name="connect_failed">链接失败</string> </resources>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="my.work.Library" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="15" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".WsnActivty" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".FlashActivity" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:screenOrientation="portrait" > <intent-filter> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".LinearLayout_activity" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:screenOrientation="portrait" > <intent-filter> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> </manifest>
Generic.java
package tools; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.Enumeration; import java.util.List; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.ThumbnailUtils; import android.os.Environment; import android.text.format.Time; import android.util.Log; import android.widget.Toast; public class Generic { public static void showMsg(Context c, String msg, boolean flag){ if(flag) /** * Toast是已经用于显示给用户的控件,显示一段时间后消失,可以多久消失 * LENGTH_SHORT:断的显示时间 * LENGTH_LONG :长的显示时间 */ Toast.makeText(c, msg, Toast.LENGTH_SHORT).show(); else Toast.makeText(c, msg, Toast.LENGTH_LONG).show(); } // get sysTime public static String getSysNowTime() { Time localTime = new Time(); localTime.setToNow(); String strTime = localTime.format("%Y-%m-%d-%H-%M-%S"); return strTime; } /** * 得到sdcard的路径 * @return 失败返回null */ public static File getSdCardFile(){ if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){ return Environment.getExternalStorageDirectory(); } return null; } /** * 获取所有连接到本wifi热点的手机IP地址 */ public static ArrayList<String> getConnectedIP() { ArrayList<String> connectedIP = new ArrayList<String>(); try { BufferedReader br = new BufferedReader(new FileReader("/proc/net/arp")); String line; br.readLine(); while ((line = br.readLine()) != null) { String[] splitted = line.split(" "); if (splitted != null && splitted.length >= 4) { String ip = splitted[0]; connectedIP.add(ip); } } } catch (Exception e) { e.printStackTrace(); } return connectedIP; } /** * 得到照片的缩略图 * @param f 照片文件 * @param w 图片缩小的目标宽度 * @param h 图片缩小的目标高度 * @return * 1.根据android提供的BitmapFactory.Options类创建并设置好options * 2.根据File获得流对象 * 3.根据BitmapFactory.decodeStream获得位图 * 4.改变图片为居中缩放,返回位图 */ public static Bitmap getShrinkedPic(File f){ Bitmap smallBitmap = null; // 直接通过图片路径将图片转化为bitmap,并将bitmap压缩,避免内存溢出 BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 10;// 图片宽高都为原来的十分之一 options.inPreferredConfig = Bitmap.Config.ARGB_4444;// 每个像素占用2byte内存 options.inPurgeable = true;// 如果 inPurgeable // 设为True的话表示使用BitmapFactory创建的Bitmap // 用于存储Pixel的内存空间在系统内存不足时可以被回收 options.inInputShareable = true; FileInputStream fInputStream; try { fInputStream = new FileInputStream(f); // 建议使用BitmapFactory.decodeStream Bitmap bitmap = BitmapFactory.decodeStream( fInputStream, null, options);// 直接根据图片路径转化为bitmap smallBitmap = ThumbnailUtils.extractThumbnail( bitmap, 64, 48);// 创建所需尺寸居中缩放的位图 } catch (FileNotFoundException e) { e.printStackTrace(); return null; } return smallBitmap; } /** * Integer值越大,则排在前面 * @author Administrator * */ public static class DescendSortByIndex implements Comparator<Integer>{ /** * @return 负数:object2<object1,正数:object2>object1,0:相等 */ @Override public int compare(Integer object1, Integer object2) { return object2.compareTo(object1); } } /** * File的最后修改时间值越大,则排在前面 * @author Administrator * */ public static class DescendSortByTime implements Comparator<File>{ /** * @return 负数:object2<object1,正数:object2>object1,0:相等 */ @Override public int compare(File object1, File object2) { return (int) (object2.lastModified() - object1.lastModified()); } } }
Mjpeg.java
package com.mjpeg.view; import java.io.IOException; import tools.Generic; import my.work.Library.R; import com.mjpeg.io.MjpegInputStream; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Typeface; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; /** * 此类继承了SurfaceView实现了SurfaceHolder.Callback接口 * SurfaceView是视图类(view)的继承类,这个视图里内嵌入了一个专门用于绘制的Surface ,可以控制这个Surface的格式和尺寸 * SurfaceView控制这个Surface的绘制位置 * surface是纵深排序(Z-ordered)的,这表明它总在自己所在窗口的后面。surfaceview提供了一个可见区域 * 只有在这个可见区域内 的surface部分内容才可见,可见区域外的部分不可见。surface的排版显示受到视图层级关系的影响 * 它的兄弟视图结点会在顶端显示,这意味者 surface的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件) * 可以通过SurfaceHolder接口访问这个surface,getHolder()方法可以得到这个接口 * surfaceview变得可见时 ,surface被创建;surfaceview隐藏前,surface被销毁;这样能节省资源。如果你要查看 surface被创建和销毁的时机 * 可以重载surfaceCreated(SurfaceHolder)和 surfaceDestroyed(SurfaceHolder) * surfaceview的核心在于提供了两个线程:UI线程和渲染线程,这里应注意: * 1> 所有SurfaceView和SurfaceHolder.Callback的方法都应该在UI线程里调用,一般来说就是应用程序主线程,渲染线程所要访问的各种变量应该作同步处理。 * 2> 由于surface可能被销毁,它只在SurfaceHolder.Callback.surfaceCreated()和 SurfaceHolder.Callback.surfaceDestroyed()之间有效, * 所以要确保渲染线程访问的是合法有效的surface * 整个过程:继承SurfaceView并实现SurfaceHolder.Callback接口 ----> SurfaceView.getHolder()获得SurfaceHolder对象(Surface控制器) * ---->SurfaceHolder.addCallback(callback)添加回调函数---->SurfaceHolder.lockCanvas()获得Canvas对象并锁定画布 * ----> Canvas绘画 ---->SurfaceHolder.unlockCanvasAndPost(Canvas canvas)结束锁定画图,并提交改变,将图形显示。 */ public class MjpegView extends SurfaceView implements SurfaceHolder.Callback { /*fps显示位置*/ public final static int POSITION_UPPER_LEFT = 9; public final static int POSITION_UPPER_RIGHT = 3; public final static int POSITION_LOWER_LEFT = 12; public final static int POSITION_LOWER_RIGHT = 6; /*图像显示模式*/ public final static int STANDARD_MODE = 1;//标准尺寸 public final static int KEEP_SCALE_MODE = 4;//保持宽高比例 public final static int FULLSCREEN_MODE = 8;//全屏 private Context mContext = null; private MjpegViewThread mvThread = null; private MjpegInputStream mIs = null; private Paint overlayPaint = null;//用于fps涂层绘画笔 private boolean bIsShowFps = true; private boolean bRun = false; private boolean bsurfaceIsCreate = false; private int overlayTextColor; private int overlayBackgroundColor; private int ovlPos; private int dispWidth;//MjpegView的宽度 private int dispHeight;//MjpegView的高度 private int displayMode;//覆盖模式 public MjpegView(Context context) { super(context); init(context); } /** * 因为在res/layout目录下的main.xml中作为自定义的控件使用了这个类,所以需要给此类提供带有属性形参的构造函数 * 当在MainActivity通过ID找到这自定义的控件时,该构造函数将被调用,所以将该构造函数设为public * @param context * @param attrs */ public MjpegView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } /** * 类的私有方法 * 1.获得Surface控制器,为Surface控制器添加回调接口 * 2.新建渲染线程MjpegViewThread * 3.新建覆盖画笔,设置文本的对齐方式、文本长度、字体、画笔文本颜色、画笔背景 * 4.设置覆盖动态文本的覆盖位置 //如果你只需要实现监控画面的功能,3和4步可以省略 * 5.设置MjpegView显示模式 * @param context */ private void init(Context context) { mContext = context; SurfaceHolder holder = getHolder(); holder.addCallback(this); mvThread = new MjpegViewThread(holder, context); setFocusable(true); overlayPaint = new Paint(); overlayPaint.setTextAlign(Paint.Align.LEFT); overlayPaint.setTextSize(12); overlayPaint.setTypeface(Typeface.DEFAULT); overlayTextColor = Color.RED; overlayBackgroundColor = Color.TRANSPARENT; ovlPos = MjpegView.POSITION_UPPER_RIGHT; displayMode = MjpegView.KEEP_SCALE_MODE; } /** * Surface的任何结构性结构性的改变(如格式,大小)将激发此方法 * 主要调用渲染线程的setSurfaceSize来设置Surface的宽和高 */ public void surfaceChanged(SurfaceHolder holder, int f, int w, int h) { mvThread.setSurfaceSize(w, h); } /** * Surface被销毁之前将激发此方法,这里只设置标记位,表示Surface“被销毁了” */ public void surfaceDestroyed(SurfaceHolder holder) { bsurfaceIsCreate = false; } /** * Surface被第一次创建后将激发此方法,这里只设置标记位,表示Surface“被创建了” */ public void surfaceCreated(SurfaceHolder holder) { bsurfaceIsCreate = true; } /** * setFps,getFps,set source都在MaiActivity使用 * @param b */ public void setFps(boolean b) { bIsShowFps = b; } public boolean getFps(){ return bIsShowFps; } public void setSource(MjpegInputStream source) { mIs = source; } /** * 开始播放线程 * 设置标记,表示“Surface被创建了”,然后调用渲染线程的的run方法启动渲染 */ public void startPlay() { if (mIs != null) { bRun = true; mvThread.start(); } } /** * 停止播放线程 * 1.先设置标记,表示"停止播放" * 2.等待播放线程的退出 * 3.关闭输入流 */ public void stopPlay() { bRun = false; boolean retry = true; while (retry) { try { mvThread.join(); retry = false; } catch (InterruptedException e) { } } //线程停止后关闭Mjpeg流(很重要) mIs.closeInstance(); } /** * mjpegview的获取位图方法,调用渲染线程的获取位图方法 * @return */ public Bitmap getBitmap(){ return mvThread.getBitmap(); } /** * 设置显示模式,在MainActivity的initview调用 * @param s */ public void setDisplayMode(int s) { displayMode = s; } /** * 既然有设置显示模式,就应该也有获得显示模式,这是java在设置方法方面的风格 * @return */ public int getDisplayMode() { return displayMode; } /** * 此渲染线程类在主类上是重点,应该重点掌握 * @author Administrator * */ public class MjpegViewThread extends Thread { private SurfaceHolder mSurfaceHolder = null; private int frameCounter = 0; private long start = 0; private Canvas c = null; private Bitmap overlayBitmap = null; private Bitmap mjpegBitmap = null; private PorterDuffXfermode mode = null; /** * 用一个变量来保存传进来的surfaceHolder * 新建一个目的图层和覆盖图层的相交模式,mjpegview为目的图层,覆盖图层为右上角的动态"文本" * mode在calculateFps方法里使用 * @param surfaceHolder:Surfaceview控制器 * @param context : 上下文环境 */ public MjpegViewThread(SurfaceHolder surfaceHolder, Context context) { mSurfaceHolder = surfaceHolder; mode = new PorterDuffXfermode(PorterDuff.Mode.DST_OVER);/*相交时动态文本覆盖mjpegview*/ } public Bitmap getBitmap(){ return mjpegBitmap; } /** * 计算图像尺寸 * @param bmw bitmap宽 * @param bmh bitmap高 * @return 图像矩阵 */ private Rect destRect(int bmw, int bmh) { int tempx; int tempy; /** * 显示模式只会在全屏和半屏模式之间切换,根本不会进入STANDARD_MODE模式,故下面的if分支可以去掉 */ if (displayMode == MjpegView.STANDARD_MODE) { tempx = (dispWidth / 2) - (bmw / 2); tempy = (dispHeight / 2) - (bmh / 2); return new Rect(tempx, tempy, bmw + tempx, bmh + tempy); } /** * 一开始,程序处于KEEP_SCALE_MODE模式,表示半屏显示画面 */ if (displayMode == MjpegView.KEEP_SCALE_MODE) { float bmasp = (float) bmw / (float) bmh; bmw = dispWidth; bmh = (int) (dispWidth / bmasp);/*宽是手机屏幕的一半*/ if (bmh > dispHeight) { bmh = dispHeight; bmw = (int) (dispHeight * bmasp); } tempx = (dispWidth / 2) - (bmw / 2); tempy = (dispHeight / 2) - (bmh / 2); /** * Rect(左边,顶边,右边,下边),功能是绘制一个特定坐标的矩形 * 简单说就是左上角坐标为(0,0),右下角坐标为(bmw,bmh) */ return new Rect(0, 0, bmw + 0, bmh + 0); } /** * 如果显示模式为全屏,则全屏显示画面 * dispWidth和dispHeight在下面的setSurfaceSize方法使用,它们表示mjpegview的宽和高 */ if (displayMode == MjpegView.FULLSCREEN_MODE) return new Rect(0, 0, dispWidth, dispHeight); return null; } /** * 当mjpegview发生任何结构性的改变时,将激发此方法,前面也提到,渲染线程使用的各种变量需做同步处理 * synchronized内的就是同步代码块,为了防止线程之间对临界资源的竞争 * @param width * @param height */ public void setSurfaceSize(int width, int height) { synchronized (mSurfaceHolder) { dispWidth = width; dispHeight = height; } } /** * 此方法被calculateFps使用,calculateFps又被渲染线程的run方法使用 * 功能是返回一个位图 * @param p:覆盖"文本"用的画笔 * @param text:要绘制的字符 如:帧 * @return bm */ private Bitmap makeFpsOverlay(Paint p, String text) { int nWidth, nHeight; Rect b = new Rect(); //int a = b.left ; /** * 功能是获得从原点开始,字符围绕的最小的矩形 * text:字符 * 0:表示第一个字符 * text.length:测量的最后一个字符 * b:用于存放获得的字符矩形 * 获得了text的边界后就可以得到矩形的宽和高 */ p.getTextBounds(text, 0, text.length(), b); nWidth = b.width() + 2; nHeight = b.height() + 2; /** * 每一个像素4字节,根据上面获得的宽和高返回一个位图 */ Bitmap bm = Bitmap.createBitmap(nWidth, nHeight, Bitmap.Config.ARGB_8888); /** * Canvas :画布,这是图像处理的基本单元 * 画图时,需要4个重要的元素: * 1.操作像素的位图 * 2.绘图到位图的画布 * 3.矩形 * 4. 描述颜色和绘制风格的画笔 * Canvas(bm):构造出一个要绘制到位图的画布 */ Canvas c = new Canvas(bm); /** * Paint类介绍 * Paint即画笔,在绘图过程中起到了极其重要的作用,画笔主要保存了颜色, * 样式等绘制信息,指定了如何绘制文本和图形,画笔对象有很多设置方法, * 大体上可以分为两类,一类与图形绘制相关,一类与文本绘制相关。 * * 1.图形绘制 * setColor(int color); * 设置绘制的颜色,使用颜色值来表示,该颜色值包括透明度和RGB颜色。 * setDither(boolean dither); * setXfermode(Xfermode xfermode); * 设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果 * * 2.文本绘制 * setFakeBoldText(boolean fakeBoldText); * 模拟实现粗体文字,设置在小字体上效果会非常差 * setSubpixelText(boolean subpixelText); * 设置该项为true,将有助于文本在LCD屏幕上的显示效果 * * setTextAlign(Paint.Align align); * 设置绘制文字的对齐方向 * setTextSize(float textSize); * 设置绘制文字的字号大小 * setTypeface(Typeface typeface); * 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等 */ p.setColor(overlayBackgroundColor);// 背景颜色 c.drawRect(0, 0, nWidth, nHeight, p);/*绘制矩形*/ p.setColor(overlayTextColor);// 文字颜色 /** * 画布的绘制文字方法 * test:要绘制的字符 * -b.left:字符起始位置的x坐标,这里是矩形的左边 * (nHeight / 2) - ((p.ascent() + p.descent()) / 2) + 1:字符起始位置的y坐标 * p:用到的画笔 * 关于涉及的矩形属性可看博客 http://mikewang.blog.51cto.com/3826268/871765 */ c.drawText(text, -b.left + 1, (nHeight / 2) - ((p.ascent() + p.descent()) / 2) + 1, p); return bm; } /** * 重头戏 * 如果线程是运行的,SurfaceView也创建了的 * 则锁定画布com/mjpeg/io/MjpegInputStream.java中的readMjpegFrame方法获得mjpeg视频流的内容 * mjpeg视频的内容就是类位图,然后根据类位图绘制矩形,再绘制相应的位图,这个位图才是我们需要的 * 如果设置了帧率文本,就在mjpegview上覆盖,最后解锁画布 */ public void run() { start = System.currentTimeMillis(); Rect destRect; Paint p = new Paint(); // String fps = ""; while (bRun) { if (bsurfaceIsCreate) { c = mSurfaceHolder.lockCanvas(); try { mjpegBitmap = mIs.readMjpegFrame();/*调用Inputstrean的方法*/ /*同步图像的宽高设置*/ synchronized (mSurfaceHolder) { destRect = destRect(mjpegBitmap.getWidth(), mjpegBitmap.getHeight()); } /** * 当主activity点击相册和设置跳转时,Surfaceview被销毁,此时c将为空 */ if(c != null){ c.drawPaint(new Paint()); c.drawBitmap(mjpegBitmap, null, destRect, p); if (bIsShowFps) calculateFps(destRect, c, p); mSurfaceHolder.unlockCanvasAndPost(c); } } catch (IOException e) { } }else { try { Thread.sleep(500);//线程休眠,让出调度 } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * 使用前面的方法,绘制出“显示帧率”文本,效果为"i帧",i自增 * @param destRect * @param c * @param p */ public void calculateFps(Rect destRect, Canvas c, Paint p) { int width; int height; String fps; p.setXfermode(mode);/* 设置两个画面相交时的模式*/ if (overlayBitmap != null) { /** * 计算好文本的宽和高 * 然后调用画布的绘制位图方法绘图 */ height = ((ovlPos & 1) == 1) ? destRect.top : destRect.bottom - overlayBitmap.getHeight(); width = ((ovlPos & 8) == 8) ? destRect.left : destRect.right - overlayBitmap.getWidth(); c.drawBitmap(overlayBitmap, width, height, null); } p.setXfermode(null); frameCounter++; /** * currentTimeMillis表示系统从January 1, 1970 00:00:00.0 UTC开始的毫秒数 * start在前面已经设置好,表示渲染线程开始的系统时间 */ if ((System.currentTimeMillis() - start) >= 1000) { fps = frameCounter+ "fps"; start = System.currentTimeMillis(); overlayBitmap = makeFpsOverlay(overlayPaint, fps);/*真正的绘制这个"文本"*/ frameCounter = 0; } } } }
MjpegInputStream.java
package com.mjpeg.io; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.util.Properties; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.CoreConnectionPNames; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Parcelable; import android.util.Log; /** * 该类继承了DataInputStream实现了Serializable接口 * 1. 实例化流,获取初始化流和关闭实例流的方法 * 2. 一个构造函数 * 3. 一个根据帧数据大小获得位图方法 */ public class MjpegInputStream extends DataInputStream implements Serializable{ /** * */ private static final long serialVersionUID = 1L; /** * 用UE打开发现 每一个jpg格式的图片 开始两字节都是 0xFF,0xD8 */ private final byte[] SOI_MARKER = { (byte) 0xFF, (byte) 0xD8 }; // private final byte[] EOF_MARKER = { (byte) 0xFF, (byte) 0xD9 }; /** * 表示服务器发给客户端的一帧数据的长度 */ private final String CONTENT_LENGTH = "Content-Length"; private final static int HEADER_MAX_LENGTH = 100; private final static int FRAME_MAX_LENGTH = 40000 + HEADER_MAX_LENGTH; private int mContentLength = -1; private static MjpegInputStream mis = null; /** * 调用该类的构造方法 创建MjpegInputStream流 * @param is */ public static void initInstance(InputStream is){ if(mis == null) mis = new MjpegInputStream(is); } /** * 获得创建的mjpegInputsteam流 * @return */ public static MjpegInputStream getInstance(){ if(mis != null) return mis; return null; } /** * 因为mpjeginputstream继承了datainputstream * 所以可以调用mpjeginputstream的关闭流方法 */ public static void closeInstance(){ try { mis.close(); } catch (IOException e) { e.printStackTrace(); } mis = null; } private MjpegInputStream(InputStream in) { super(new BufferedInputStream(in, FRAME_MAX_LENGTH)); } /** * 在数据流里面找SOI_MARKER={(byte)0xFF,(byte) 0xD8} * 所有对IO流的操作都会抛出异常 * @param in * @param sequence * @return * @throws IOException */ private int getEndOfSeqeunce(DataInputStream in, byte[] sequence) throws IOException { int seqIndex = 0; byte c; for (int i = 0; i < FRAME_MAX_LENGTH; i++) {// 0 1 2 3 c = (byte) in.readUnsignedByte(); if (c == sequence[seqIndex]) { seqIndex++; if (seqIndex == sequence.length)//2 return i + 1;//3 } else seqIndex = 0; } return -1; } /** * 此方法功能是找到索引0xFF,0XD8在字符流的位置 * 整个数据流形式:http头信息 帧头(0xFF 0xD8) 帧数据 帧尾(0xFF 0xD9) * 1、首先通过0xFF 0xD8找到帧头位置 * 2、帧头位置前的数据就是http头,里面包含Content-Length,这个字段指示了整个帧数据的长度 * 3、帧头位置后面的数据就是帧图像的开始位置 * @param in * @param sequence * @return * @throws IOException */ private int getStartOfSequence(DataInputStream in, byte[] sequence) throws IOException { int end = getEndOfSeqeunce(in, sequence); return (end < 0) ? (-1) : (end - sequence.length); } /** * 从http的头信息中获取Content-Length,知道一帧数据的长度 * @param headerBytes * @return * @throws IOException * @throws NumberFormatException */ private int parseContentLength(byte[] headerBytes) throws IOException, NumberFormatException { /** * 根据字节流创建ByteArrayInputStream流 * Properties是java.util包里的一个类,它有带参数和不带参数的构造方法,表示创建无默认值和有默认值的属性列表 * 根据流中的http头信息生成属性文件,然后找到属性文件CONTENT_LENGTH的value,这就找到了要获得的帧数据大小 * 创建一个 ByteArrayInputStream,使用 headerBytes作为其缓冲区数组 */ ByteArrayInputStream headerIn = new ByteArrayInputStream(headerBytes); Properties props = new Properties();/*创建一个无默认值的空属性列表*/ props.load(headerIn);/*从输入流中生成属性列表(键和元素对)。*/ return Integer.parseInt(props.getProperty(CONTENT_LENGTH));/*用指定的键在此属性列表中搜索属性。*/ } /** * * @return * @throws IOException */ public Bitmap readMjpegFrame() throws IOException { mark(FRAME_MAX_LENGTH);/*流中当前的标记位置*/ int headerLen = getStartOfSequence(this, SOI_MARKER); reset();/*将缓冲区的位置重置为标记位置*/ byte[] header = new byte[headerLen]; readFully(header);/*会一直阻塞等待,直到数据全部到达(数据缓冲区装满)*/ // String s = new String(header); try { mContentLength = parseContentLength(header);// ? } catch (NumberFormatException e) { return null; } /** * 根据帧数据的大小创建字节数组 */ byte[] frameData = new byte[mContentLength]; readFully(frameData); /** * 根据不同的源(file,stream,byte-arrays)创建位图 * 把输入字节流流转为位图 */ return BitmapFactory.decodeStream(new ByteArrayInputStream(frameData)); } }
WsnActivty.java
package my.work.Library; import java.util.Timer; import java.util.TimerTask; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; public class WsnActivty extends Activity { /** Called when the activity is first created. */ private Button btnNetwork,btnVideo; private String strIpAddr = null; static TextView textTips,seat; private ClientThread clientThread = null; private Message MainMsg; public static Handler mainHandler; static final int TIPS_UPDATE_UI = 3; //tips_update_ui static final int SEAT_UPDATE_UI = 6; //seat_update_ui static final int MAX_NODE = 4; static byte NodeData[][] = new byte[MAX_NODE][5];; // [5] 0=温度 1=湿度 2=气体 3=灯 static final int RX_DATA_UPDATE_UI = 1; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); initControl(); initMainHandler(); } private void initControl() { // TODO Auto-generated method stub btnNetwork = (Button) findViewById(R.id.btn_network); btnNetwork.setOnClickListener(new ButtonClick()); textTips = (TextView) findViewById(R.id.Tips); textTips.setText(R.string.init_tips); seat = (TextView) findViewById(R.id.statc001); seat.setText(R.string.people1); btnVideo = (Button) findViewById(R.id.btn_video); btnVideo.setOnClickListener(new ButtonClick()); } class ButtonClick implements OnClickListener { @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_network: // 连接网络 showDialog(WsnActivty.this); break; case R.id.btn_video: // 暂时用作停止自动刷新功能 ,跳转到FlashActivity去 // mainTimer.cancel(); //看视频关闭定时查询数据 要判断 if (clientThread != null) { MainMsg = ClientThread.childHandler .obtainMessage(ClientThread.RX_EXIT); //关闭线程 ClientThread.childHandler.sendMessage(MainMsg); } /*new一个Intent对象,并指定class*/ Intent intent = new Intent(); intent.setClass(WsnActivty.this,FlashActivity.class); /*new一个Bundle对象,并将要传递的数据传入*/ Bundle bundle = new Bundle(); bundle.putString("IP",strIpAddr); /*将Bundle对象assign给Intent*/ intent.putExtras(bundle); /*调用Activity FlashActivity*/ startActivity(intent); //startActivity(new Intent(WsnActivity.this, FlashActivity.class));//简单跳转 break; } } } // 显示连接对话框 private void showDialog(Context context) { final EditText editIP = new EditText(context); editIP.setText("192.168.0.10"); AlertDialog.Builder builder = new AlertDialog.Builder(context); // builder.setIcon(R.drawable.ic_launcher); builder.setTitle("请输入服务器IP地址"); builder.setView(editIP); builder.setPositiveButton("连接", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { strIpAddr = editIP.getText().toString(); boolean ret = isIPAddress(strIpAddr); if (ret) { textTips.setText("服务器IP地址:" + strIpAddr); } else { strIpAddr = null; textTips.setText("IP地址不合法,请重新设置"); return; } clientThread = new ClientThread(strIpAddr);// 建立客户端线程 clientThread.start(); //mainTimer = new Timer();// 定时查询所有终端信息 //setTimerTask(); } }); builder.setNeutralButton("取消", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { if (clientThread != null) { MainMsg = ClientThread.childHandler .obtainMessage(ClientThread.RX_EXIT); ClientThread.childHandler.sendMessage(MainMsg); textTips.setText("与服务器断开连接"); } } }); builder.show(); } // 判断输入IP是否合法 private boolean isIPAddress(String ipaddr) { boolean flag = false; Pattern pattern = Pattern .compile("\\b((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\b"); Matcher m = pattern.matcher(ipaddr); flag = m.matches(); return flag; } void initMainHandler() { mainHandler = new Handler() { // 主线程消息处理中心 public void handleMessage(Message msg) { switch (msg.what) { case TIPS_UPDATE_UI: String str = (String) msg.obj; //连接成功 textTips.setText(str); break; case SEAT_UPDATE_UI: String strseat = (String) msg.obj; //rfid seat.setText(strseat); break; } super.handleMessage(msg); } }; } }
ClientThread.java
package my.work.Library; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.widget.Toast; public class ClientThread extends Thread { private OutputStream outputStream = null; private InputStream inputStream = null; private Socket socket; private SocketAddress socketAddress; public static Handler childHandler; private boolean RxFlag = true; final int TEXT_INFO = 12; static final int RX_EXIT = 11; static final int TX_DATA = 10; Context mainContext; Message msg; private String strIP; final int SERVER_PORT = 33333; byte cNodeData[][] = new byte[4][5]; // [5] 0=温度 1=湿度 2=气体 3=灯 int RxCount = 0, nRecvLen, index = 0; byte CheckSum; // byte strRxBuf[] = new byte[256]; int ucRecvLen = 7; private RxThread rxThread; //获取WsnActivty.java 开辟子线程clientThread对象线程传递过来的ip地址 public ClientThread(String ip) { strIP = ip; } // 连接网络 void connect() { RxFlag = true; socketAddress = new InetSocketAddress(strIP, SERVER_PORT); socket = new Socket(); try { socket.connect(socketAddress, SERVER_PORT); inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); msg = WsnActivty.mainHandler.obtainMessage( WsnActivty.TIPS_UPDATE_UI, "连接成功"); WsnActivty.mainHandler.sendMessage(msg); rxThread = new RxThread(); rxThread.start(); } catch (IOException e) { try { sleep(10); } catch (InterruptedException e1) { e1.printStackTrace(); } msg = WsnActivty.mainHandler.obtainMessage( WsnActivty.TIPS_UPDATE_UI, "无法连接到服务器"); WsnActivty.mainHandler.sendMessage(msg); e.printStackTrace(); } catch (NumberFormatException e) { } } void initChildHandler() { Looper.prepare(); // 在子线程中创建Handler必须初始化Looper childHandler = new Handler() { // 子线程消息处理中心 public void handleMessage(Message msg) { // 接收主线程及其他线程的消息并处理... /** * MainMsg = ClientThread.childHandler.obtainMessage(ClientThread.TX_DATA, * len, 0, (Object) buffer); * SendData(SendBuf, 7); */ switch (msg.what) { case RX_EXIT: RxFlag = false; try { if (socket.isConnected()) { inputStream.close(); outputStream.close(); socket.close(); } } catch (IOException e1) { e1.printStackTrace(); } childHandler.getLooper().quit();// 结束消息队列 break; default: break; } } }; // 启动该线程的消息队列 Looper.loop(); } public void run() { connect(); initChildHandler(); msg = WsnActivty.mainHandler.obtainMessage(WsnActivty.TIPS_UPDATE_UI, "与服务器断开连接"); WsnActivty.mainHandler.sendMessage(msg); } // socket 接收线程 public class RxThread extends Thread { public void run() { try { while (socket.isConnected() && RxFlag) { byte strRxBuf[] = new byte[30]; byte i; int RxIndex, len, readBytes = 0; while (readBytes < ucRecvLen) { //接收到数据,存放到strRxBuf len = inputStream.read(strRxBuf,readBytes, 8); readBytes += len; if (len == -1) break; } String strRead =new String(strRxBuf); String str=new String("822350C2"); String str1="822350C2"; if(str.equals(str1)) { msg = WsnActivty.mainHandler.obtainMessage( WsnActivty.SEAT_UPDATE_UI, "有人"); WsnActivty.mainHandler.sendMessage(msg); } msg = WsnActivty.mainHandler.obtainMessage( WsnActivty.SEAT_UPDATE_UI, strRead); WsnActivty.mainHandler.sendMessage(msg); } if (socket.isConnected()) socket.close(); } catch (IOException e) { e.printStackTrace(); } } } byte GetDataLen(byte fc) { byte len = 0; switch (fc) { case 0x01: len = 22; break; case 0x07: case 0x08: case 0x0A: case 0x0B: case 0x0C: case 0x0D: len = 7; break; } return len; } }
FlashActivity.java
package my.work.Library; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import my.work.Library.WsnActivty.ButtonClick; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.CoreConnectionPNames; import tools.Generic; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.net.DhcpInfo; import android.net.wifi.WifiManager; import android.os.AsyncTask; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import com.mjpeg.io.MjpegInputStream; /** * 应用程序执行时,该类首先被调用 */ public class FlashActivity extends Activity { private Context mContext = this; private EditText ipEdt = null; private EditText portEdt = null; private TextView hintTv = null; private DhcpInfo dpInfo = null; private WifiManager wifi = null; private InputStream is = null; private SharedPreferences sp = null; private Editor editor = null; private String port = "8080";/* 用来保存获得用户输入的端口 */ private Bundle bundle; private Button connectin; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.flash);/* 设置布局为res/layout/flash.xml*/ init(); int state = wifi.getWifiState();/* 获得wifi当前状态 */ if (state != WifiManager.WIFI_STATE_ENABLED) { /** * 为了程序的扩展性和可读性,单独在tools目录定义一个Generic类,它有很多方法 * 1.有showMsg方法,用于控制显示时间来显示一个Toast * 2.有getSysNowTime方法,用于获取当前的系统时间 * 3.有getSdCardFile方法,用于获取SD卡的绝对路径,成功返回File值,失败返回NULL * 4.有getConnectedIP方法,用于获取连接到wifi热点的所有的手机ip,成功返回ArrayList<String>型的容器 * 5.有getShrinkedPic方法,用于获取照片的缩略图 * 6.定义了一个DescendSortByIndex类:实现了整型比较器 * 7.定义个DescendSortByTime类:实现了File比较器 */ Generic.showMsg(this, "请打开wifi", false); finish(); } else /* 取得Intent中的Bundle对象 */ bundle = this.getIntent().getExtras(); /* 取得Bundle对象中的数据 */ String strIP = bundle.getString("IP"); //autoConnect(strIP); } @Override /** * 调用finish方法时,这方法将被激发 * 设置输入流为空,调用父类的onDestroy销毁资源 */ protected void onDestroy() { is = null; super.onDestroy(); } private void init() { /** * 获取在本Activity要使用的控件和WiFi */ hintTv = (TextView) findViewById(R.id.hintTv); ipEdt = (EditText) findViewById(R.id.ip); portEdt = (EditText) findViewById(R.id.port); connectin = (Button) findViewById(R.id.connect); connectin.setOnClickListener(new ButtonClick()); /** * 因为要用到WIFI和Internet所以在AndroidMenufest.xml 中添加如下权限 <uses-permission * android:name="android.permission.INTERNET"/> <uses-permission * android:name="android.permission.ACCESS_WIFI_STATE"/> * <uses-permission * android:name="android.permission.CHANGE_WIFI_STATE"/> */ wifi = (WifiManager) getSystemService(Context.WIFI_SERVICE); //initSp();/* 主要是方便查找以前登录成功了的IP */ } class ButtonClick implements OnClickListener { @Override public void onClick(View v) { String ip = ipEdt.getText().toString();/* 获得输入的IP */ port = portEdt.getText().toString();/* 获得输入的端口 */ // port不能为空,ip地址格式正确 if (!port.equals("") && checkAddr(ip, Integer.valueOf(port))) { new ConnectTask().execute(ip); } else { Generic.showMsg(mContext, "请检查ip或port", true); } } } // /** // * 生成配置文件config,它在 /data/data/<package name>/shared_prefs/config.xml // * 取出配置文件的ip用冒号隔开,并为自动完成列表设置适配器 // */ // private void initSp() { // sp = getSharedPreferences("config", MODE_PRIVATE); // /* 创建好配置文件后,以后就可以用它的edit来操作配置文件了 */ // editor = sp.edit(); // String names[] = sp.getString("ip", "").split(":"); // ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext, // android.R.layout.simple_dropdown_item_1line, names); // //ipEdt.setAdapter(adapter); // } // // /** // * 自动连接 先将获取到的wifi热点服务器地址和连接到wifi热点的设备的ip放入容器,启动连接线程扫描容器中的ip // * // * @return // */ // private void autoConnect(String strIP) { // ArrayList<String> addr = new ArrayList<String>();/* 创建容器 用于存放ip */ // // dpInfo = wifi.getDhcpInfo(); // addr.add(int32ToIp(dpInfo.serverAddress));/* 把服务IP放入容器的尾部 */ // addr.addAll(Generic.getConnectedIP());// Adds the objects in the specified collection to this ArrayList // // // 为了在执行连接时 不会卡住UI,故采用异步任务方式,若读者想减缩程序,也可不使用异步任务 // if (strIP != null) { // new ConnectTask().execute(strIP); // } else { // //因为连接线程的执行方法必须String类型,所以要toArray // new ConnectTask().execute(addr.toArray(new String[addr.size()])); // } // } /** * 按照一定的格式返回输入的Ip * * @param ip * @return */ private String int32ToIp(int ip) { return (ip & 0xff) + "." + (ip >> 8 & 0xff) + "." + (ip >> 16 & 0xff) + "." + (ip >> 24 & 0xff); } // /** // * 手动连接 为控件绑定监听器有2种方法 1.给出布局文件并设置,findViewById()找到控件,调用API为其绑定相应监听器 // * 2.给出布局文件并设置,在布局文件里设置相应控件的OnClick,然后在源文件里具体实现相应控件的OnClick//本类用的就是这方法 // * 在layout目录下的flash.xml里声明了connectBtn的Button控件 点击"连接"按钮将调用此方法 // * // * @param v // */ // public void connectBtn() { // String ip = ipEdt.getText().toString();/* 获得输入的IP */ // port = portEdt.getText().toString();/* 获得输入的端口 */ // // // port不能为空,ip地址格式正确 // if (!port.equals("") && checkAddr(ip, Integer.valueOf(port))) { // new ConnectTask().execute(ip); // } else { // Generic.showMsg(mContext, "连接失败", true); // } // } /** * 分割的ip是4段,ip端口范围在1000-65535 * * @param ip * @param port * @return */ private boolean checkAddr(String ip, int port) { if (ip.split("\\.").length != 4) return false; if (port < 1000 || port > 65535) return false; return true; } /** * 连接线程 此类的作用是在后台线程里执行http连接,连接卡住不会影响UI运行,适合于运行时间较长但又不能影响前台线程的情况 * 异步任务,有3参数和4步 * :onPreExecute(),doInBackground(),onProgressUpdate(),onPostExecute() * onPreExecute():运行于UI线程,一般为后台线程做准备,如在用户接口显示进度条 * doInBackground():当onPreExecute执行后,马上被触发,执行花费较长时间的后台运算,将返回值传给onPostExecute * onProgressUpdate():当用户调用 publishProgress()将被激发,执行的时间未定义,这个方法可以用任意形式显示进度 * 一般用于激活一个进度条或者在UI文本领域显示logo onPostExecute():当后台进程执行后在UI线程被激发,把后台执行的结果通知给UI * 参数一:运行于后台的doInBackground的参数类型 * 参数二:doInBackground计算的通知给UI线程的单元类型,即运行于UI线程onProgressUpdate的参数类型,这里没用到 * 参数三:doInBackground的返回值,将传给onPostExecute作参数 * * @author Administrator * */ private class ConnectTask extends AsyncTask<String, Integer, String> { @Override protected String doInBackground(String... params) { for (int i = 0; i < params.length; i++) { String ip = params[i];/* 取出每一个ip */ if (ip.split("\\.").length == 4) { /** * 在浏览器观察画面时,也是输入下面的字符串网址 */ String action = "http://" + ip + ":" + port + "/?action=stream"; is = http(action); if (is != null) { /* 第一次必须输入IP,下次登录时才可找到之前登录成功后的IP */ //writeSp(ip); MjpegInputStream.initInstance(is); //消息实体内容is break; } } } return null; } @Override protected void onPostExecute(String result) { if (is != null) { /** * Intent是Android特有的东西,可以在Intent指定程序要执行的动作(比如:view,edit,dial) * 都准备好程序执行该工作所需要的材料后 * ,只要调用startActivity,Android系统会自动寻找最符合你指定要求的应用程序 并执行该程序 */ startActivity(new Intent(FlashActivity.this, LinearLayout_activity.class)); finish();/* 结束本Activity */ } else { hintTv.setText(getResources() .getString(R.string.connect_failed)); Generic.showMsg(mContext, "连接失败", true); } super.onPostExecute(result); } /** * 功能:http连接 Android提供两种http客户端, HttpURLConnection 和 Apache HTTP * Client,它们都支持HTTPS,能上传和下载文件 配置超时时间,用于IPV6和 connection pooling, Apache * HTTP client在Android2.2或之前版本有较少BUG * 但在Android2.2或之后,HttpURLConnection是更好的选择,在这里我们用的是 Apache HTTP Client * 凡是对IO的操作都会涉及异常,所以要try和catch * * @param url * @return InputStream */ private InputStream http(String url) { HttpResponse res; DefaultHttpClient httpclient = new DefaultHttpClient();/* * 创建http客户端, * 才能调用它的各种方法 */ httpclient.getParams().setParameter( CoreConnectionPNames.CONNECTION_TIMEOUT, 500);/* 设置超时时间 */ try { HttpGet hg = new HttpGet(url);/* * 这是GET方法的http API, * GET方法是默认的HTTP请求方法 */ res = httpclient.execute(hg); return res.getEntity().getContent(); // 从响应中获取消息实体内容 } catch (IOException e) { } return null; } } } // /** // * 更新SharedPreferences 1.先判断ip是否有"ip"值,没有就将传进来的data赋值给ip 2.ip有值就取出,然后用冒号分隔开 // * 3.sp数组只能存放10组ip,如果超过了10组,先清零配置文件再更新 4.遍历数组,如果已有当前登录成功的ip,则返回 // * 5.数组里不包含登录成功的ip,则将当前登录成功的ip添加至sp数组并提交 // * // * @param ip // */ // private void writeSp(String data) { // if (!sp.contains("ip")) { // editor.putString("ip", data); // editor.commit(); // return; // } // // /** // * 配置文件里有ip,表示之前登录成功了 // */ // String ip = sp.getString("ip", ""); // String[] ips = ip.split(":"); // // if (ips.length >= 10) { // editor.clear(); // editor.commit(); // editor.putString("ip", data); // editor.commit(); // return; // } // // for (int i = 0; i < ips.length; i++) { // if (ips[i].equals(data)) // return; // } // editor.putString("ip", data + ":" + ip);/* 放在以前成功了的ip的前面 */ // editor.commit(); // } // // /** // * 自动完成框的下拉选项 当点击"history_user"ImageView控件时将调用该方法 这里只是具体实现xml文件的Onclick // */ // public void showDropDown(View v) { // ipEdt.showDropDown(); // }
LinearLayout_activity.java
package my.work.Library; import com.mjpeg.io.MjpegInputStream; import com.mjpeg.view.MjpegView; import android.app.Activity; import android.os.Bundle; import my.work.Library.R; public class LinearLayout_activity extends Activity { public static LinearLayout_activity instance = null; private MjpegInputStream mis = null; private MjpegView mjpegView = null; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.mainactivty);/*构造RadioGroup的5的RadioButton*/ instance = this; mis = MjpegInputStream.getInstance(); mjpegView = (MjpegView) findViewById(R.id.mjpegview); initMjpegView(); } private void initMjpegView() { if (mis != null) { mjpegView.setSource(mis);// 设置数据来源 mjpegView.setDisplayMode(mjpegView.getDisplayMode());/*设置mjpegview的显示模式*/ /** * setFps和getFps方法是为了在屏幕的右上角动态显示当前的帧率 * 如果我们只需观看画面,下面这句完全可以省去 */ mjpegView.setFps(mjpegView.getFps()); /** * 调用mjpegView中的线程的run方法,开始显示画面 */ mjpegView.setDisplayMode(MjpegView.FULLSCREEN_MODE);/*全屏*/ mjpegView.startPlay(); } } }