編輯:關於Android編程
最近無聊,做一下android音視頻方面的學習和研究。網上有很多案例值得我們去學習。我個人先去大概了解了一下音視頻的一些基本概念,然後做了一個小demo,實現了一台手機錄制,另一台手機實時播放。視屏錄制采用的Camera,預覽用的SurfaceView,把采集的視頻壓縮成bitmap,通過socket進行傳輸到另外一台手機上。這個demo只是用來學習android Camera,bitmap傳輸比較耗流量,所以該demo僅供學習。
視頻采集客戶端:
錄制頁面activity
package com.fm.camero; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import com.fangming.myframwork.R; import com.fm.qxtapp.MyApp; import android.content.Intent; import android.content.res.Configuration; import android.graphics.ImageFormat; import android.graphics.Rect; import android.graphics.YuvImage; import android.hardware.Camera; import android.hardware.Camera.PreviewCallback; import android.hardware.Camera.Size; import android.os.Bundle; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.SurfaceHolder.Callback; import android.view.View.OnClickListener; import android.view.Window; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; /** * @describe 視頻錄制成圖片壓縮socket傳輸 * @author fangming * @CreateTime 2016年8月16日下午2:32:41 */ public class TestcamoraActivety2 extends ServiceActivity implements Callback, PreviewCallback { private TestcamoraActivety2 _this; private String TAG = TestcamoraActivety2.class.getSimpleName(); private Button btn_start; private SurfaceView sf_surfaceView; private SurfaceHolder suHolder; private int width;// 分辨率寬度 private int height;// 分辨率高度 private Camera mCamera; private boolean isStreaming = false; private MyApp myApp; private Button btn_puse; private ImageView iv_show; private Boolean isShow = true; int mwidth, mheight; private Button btn_switch; private Button btn_conn; private Button btn_disconn; private EditText et_ip; private EditText et_port; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_testcamora); _this = this; initView(); myApp = MyApp.getInstance(); } private void initSurfaceholder() { suHolder = sf_surfaceView.getHolder(); suHolder.addCallback(this); // width = 352; // height = 288; width = 320; height = 480; mwidth = width; mheight = height; System.out.println("*****相機初始化完成*****"); } private void initView() { btn_start = (Button) findViewById(R.id.btn_start); btn_puse = (Button) findViewById(R.id.btn_puse); iv_show = (ImageView) findViewById(R.id.iv_show); sf_surfaceView = (SurfaceView) findViewById(R.id.sf_surfaceView); initSurfaceholder(); btn_start.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mCamera.startPreview(); // new DumpFrameTask().execute(); } }); btn_puse.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mCamera.stopPreview(); } }); btn_switch = (Button) findViewById(R.id.btn_switch); btn_switch.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { } }); btn_conn = (Button) findViewById(R.id.btn_conn); btn_disconn = (Button) findViewById(R.id.btn_disconn); et_ip = (EditText) findViewById(R.id.et_ip); et_port = (EditText) findViewById(R.id.et_port); btn_conn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ip = et_ip.getText().toString(); port = et_port.getText().toString(); bindMyService(); } }); btn_disconn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { unBindMyService(); } }); } @Override public void onPreviewFrame(byte[] data, Camera camera) { isStreaming = true; System.out.println("*****相機采集到的數組長度" + data.length + "*****"); if (isShow) { mwidth = camera.getParameters().getPreviewSize().width; mheight = camera.getParameters().getPreviewSize().height; isShow = false; } YuvImage image = new YuvImage(data, ImageFormat.NV21, mwidth, height, null); if (image != null) { ByteArrayOutputStream outstream = new ByteArrayOutputStream(); image.compressToJpeg(new Rect(0, 0, mwidth, height), 80, outstream); byte[] arr=outstream.toByteArray(); // Bitmap bitmap =BitmapFactory.decodeByteArray(arr,0, arr.length); // iv_show.setRotation(90); // iv_show.setImageBitmap(bitmap); mService.sendDate(arr); try { outstream.flush(); } catch (IOException e) { e.printStackTrace(); } } } @Override public void surfaceCreated(SurfaceHolder holder) { mCamera = Camera.open(); try { mCamera.setPreviewDisplay(suHolder); } catch (IOException e1) { e1.printStackTrace(); } mCamera.setPreviewCallback(this); ListpreviewSizes = mCamera.getParameters().getSupportedPreviewSizes(); width = previewSizes.get(0).width; height = previewSizes.get(0).height; Camera.Parameters parameters = mCamera.getParameters(); parameters.setPreviewSize(width, height); // 橫豎屏鏡頭自動調整 if (this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) { parameters.set("orientation", "portrait"); // parameters.set("rotation", 90); // 鏡頭角度轉90度(默認攝像頭是橫拍) mCamera.setDisplayOrientation(90); // 在2.2以上可以使用 } else// 如果是橫屏 { parameters.set("orientation", "landscape"); // mCamera.setDisplayOrientation(0); // 在2.2以上可以使用 } System.out.println("*****surfaceCreated*****"); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { System.out.println("*****系統執行surfaceChanged*****"); } @Override public void surfaceDestroyed(SurfaceHolder holder) { System.out.println("*****surfaceDestroyed*****"); if (mCamera != null) { mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); mCamera = null; } } @Override protected void onDestroy() { super.onDestroy(); unBindMyService(); }; @Override public void finish() { super.finish(); Intent mintent = new Intent(); mintent.setAction(ConnectionService.ACTION); stopService(mintent); } }
該activity布局:
在這個錄制中,遇到的幾個比較坑的地方,第一個是視頻的寬和高,沒有設置好,播放時會導致黑屏或者花屏。還有就是對於camera采集的視頻格式的一些問題。默認是ImageFormat.NV21,也就是YUV420sp。
// yuv420P(YV12)或者yuv420SP(NV21/NV12)
parameters.setPreviewFormat(ImageFormat.NV21);
我們采集的視頻最後通過onPreviewFrame這個回調接口獲得
@Override public void onPreviewFrame(byte[] data, Camera camera) { isStreaming = true; System.out.println("*****相機采集到的數組長度" + data.length + "*****"); if (isShow) { mwidth = camera.getParameters().getPreviewSize().width; mheight = camera.getParameters().getPreviewSize().height; isShow = false; } YuvImage image = new YuvImage(data, ImageFormat.NV21, mwidth, height, null); if (image != null) { ByteArrayOutputStream outstream = new ByteArrayOutputStream(); image.compressToJpeg(new Rect(0, 0, mwidth, height), 80, outstream); byte[] arr=outstream.toByteArray(); // Bitmap bitmap =BitmapFactory.decodeByteArray(arr,0, arr.length); // iv_show.setRotation(90); // iv_show.setImageBitmap(bitmap); //視頻發送 mService.sendDate(arr); try { outstream.flush(); } catch (IOException e) { e.printStackTrace(); } } }
采集到的視頻通過mService.sendDate(arr);進行發送。mService是我自己定義的一個service用來後台處理數據。
ConnectionService類
package com.fm.camero; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.net.SocketException; import com.fm.constants.Constants; import com.fm.utill.ByteConvertUtils; import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.IBinder; import android.util.Log; public class ConnectionService extends Service { public final static String ACTION = "com.connectionService"; private final static int PACKATHEADER_SIGN_LENGTH = 2;// 我的包標識 private final static int PACKATHEADER_TYPE_LENGTH = 4;// 包類型 4byte private final static int PACKATHEADER_DATALEN_LENGTH = 4;// 包長度 4byte private final static int PACKATHEADER_LENGTH = PACKATHEADER_SIGN_LENGTH + PACKATHEADER_TYPE_LENGTH + PACKATHEADER_DATALEN_LENGTH;// 包頭 private String ipAdd = "192.168.1.109"; private int port = 6001; private String TAG = "ConnectionService"; private Socket mSocket; private OutputStream outStream; private InputStream inStream; private InitSocketThread initSocketThread; private Boolean isAccepte = true; @Override public void onCreate() { super.onCreate(); Log.e(TAG, "---->" + "onCreateservice"); } @Override public void onDestroy() { Log.e(TAG, "---->service停止"); super.onDestroy(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.e(TAG, "---->" + "onStartCommand"); return super.onStartCommand(intent, flags, startId); } /** * 初始化socket線程 */ class InitSocketThread extends Thread { @Override public void run() { super.run(); initSocket(); } } /** * 初始化socket */ private synchronized void initSocket() { Log.e(TAG, "---->創建socket成功"); try { mSocket = new Socket(ipAdd, port); if (mSocket.isConnected()) { Log.e(TAG, "---->連接成功"); outStream = mSocket.getOutputStream(); inStream = mSocket.getInputStream(); } } catch (SocketException e) { Log.d(TAG, "socket conn fail"); e.printStackTrace(); } catch (IOException e) { Log.d(TAG, "socket conn fail"); e.printStackTrace(); } } public class MyBinder extends Binder { public ConnectionService getService() { return ConnectionService.this; } } public MyBinder mBinder = new MyBinder(); @Override public IBinder onBind(Intent intent) { return mBinder; } public void sendDate(byte[] data) { int datalength = data.length; byte[] buffer = new byte[PACKATHEADER_LENGTH + datalength]; // 設置包頭3000 System.arraycopy(Constants.APPREQUEST_HEAD, 0, buffer, 0, 2); // 設置type byte[] type = { 0, 0, 0x26, (byte) 0x48 }; System.arraycopy(type, 0, buffer, 2, 4); // 設置長度 byte[] len = ByteConvertUtils.intToBytes(datalength); System.arraycopy(len, 0, buffer, 6, 4); // 設置內容 System.arraycopy(data, 0, buffer, 10, datalength); new sendDataThread(buffer).start(); } public class sendDataThread extends Thread { byte byteBuffer[] = null; public sendDataThread(byte[] data) { this.byteBuffer = data; } public void run() { try { outStream.write(byteBuffer, 0, byteBuffer.length); System.out.println("發送的數據長度:" + byteBuffer.length); outStream.flush(); } catch (IOException e) { e.printStackTrace(); } } } public void setIpAndPort(String ip, String port) { if (ip != null && !ip.equals("")) { ipAdd = ip; } if (port != null && !port.equals("")) { this.port = Integer.valueOf(port); } if (initSocketThread == null) { initSocketThread = new InitSocketThread(); initSocketThread.start(); } } }
以上就是客戶端錄制,下面是播放客戶端
播放端activity
package com.fm.server; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.Toast; public class MainActivity extends Activity { private MainActivity _this; private ImageView iv_btmap; private EditText et_port; private Button btn_bind; private Button btn_unbind; public String mPort; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); _this = this; initView(); } private void initView() { iv_btmap = (ImageView) findViewById(R.id.iv_btmap); et_port = (EditText) findViewById(R.id.et_port); btn_bind = (Button) findViewById(R.id.btn_bind); btn_unbind = (Button) findViewById(R.id.btn_unbind); btn_bind.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String port = et_port.getText().toString(); if (port == null || port.equals("")) { Toast.makeText(_this, "端口為null", Toast.LENGTH_SHORT).show(); return; } mPort = port; bindService(); } }); btn_unbind.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { unBindService(); } }); } Handler mHandler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case 0: byte[] data = (byte[]) msg.obj; if (data == null) { return; } System.out.println("handle收到的數據:" + data.length); BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2; // Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, // data.length, options); Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); if (bitmap != null) { iv_btmap.setRotation(90); iv_btmap.setImageBitmap(bitmap); } break; default: break; } }; }; @Override public void finish() { super.finish(); if (mService != null) { unBindService(); } } public void bindService() { Intent mintent = new Intent(); mintent.setClass(_this, BindService.class); bindService(mintent, conn, Context.BIND_AUTO_CREATE); } public void unBindService() { if (mService != null) { unbindService(conn); } } public BindService mService = null; ServiceConnection conn = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { mService = null; } @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = ((BindService.MyBinder) service).getService(); mService.startConn(mPort); mService.setHandler(mHandler); } }; }
播放端service
package com.fm.server;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import com.fm.constants.Constants;
import com.fm.utill.ByteConvertUtils;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
public class BindService extends Service {
private String TAG = “BindService”;
public int port = 6001;
public OutputStream outStream;
public InputStream inStream;
public Socket mSocket;
private InitSocketThread initSocketThread;
private ReadThread readThread;
private Boolean isAccepte = true;
ServerSocket server;
private final static int PACKATHEADER_SIGN_LENGTH = 2;// 我的包標識
private final static int PACKATHEADER_TYPE_LENGTH = 4;// 包類型 4byte
private final static int PACKATHEADER_DATALEN_LENGTH = 4;// 包長度 4byte
private final static int PACKATHEADER_LENGTH = PACKATHEADER_SIGN_LENGTH + PACKATHEADER_TYPE_LENGTH
+ PACKATHEADER_DATALEN_LENGTH;// 包頭
public void setPort(int port) {
this.port = port;
}
private Handler hander;
public void setHandler(Handler hander) {
this.hander = hander;
}
public void startConn(String port) {
if (port != null && !port.equals("")) {
this.port = Integer.valueOf(port);
}
initSocketThread = new InitSocketThread();
initSocketThread.start();
}
/**
* 初始化socket線程
*/
class InitSocketThread extends Thread {
@Override
public void run() {
super.run();
initSocket();
}
}
/**
* 初始化socket
*/
private void initSocket() {
Log.e(TAG, "---->創建socket成功");
try {
server = new ServerSocket(port);
mSocket = server.accept();
if (mSocket.isConnected()) {
Log.e(TAG, "---->連接成功");
outStream = mSocket.getOutputStream();
inStream = mSocket.getInputStream();
readThread = new ReadThread();
readThread.start();
}
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
class ReadThread extends Thread {
@Override
public void run() {
super.run();
while (isAccepte) {
if (mSocket != null && mSocket.isConnected()) {
devide(new DataInputStream(new BufferedInputStream(inStream)));
}
}
}
}
public synchronized void devide(DataInputStream ds) {
try {
if (ds.available() > 0) {
recv(ds);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void recv(DataInputStream dis) throws IOException {
byte[] head = new byte[PACKATHEADER_LENGTH];
Log.e(TAG, "---->>接收數據的長度" + dis.available());
try {
dis.readFully(head, 0, PACKATHEADER_LENGTH);
} catch (IOException e) {
return;
}
// 讀取前兩個字節我的數據包標識head[]
byte[] sign = new byte[2];
sign = Arrays.copyOfRange(head, 0, 2);
if (ByteConvertUtils.bytesToInt2(sign) != Constants.APP_CTL_HEAD) {
return;
}
// 讀取4byte包類型
byte[] type = new byte[4];
type = Arrays.copyOfRange(head, 2, 6);
int nType = ByteConvertUtils.bytesToInt(type);
// 讀取4byte內容長度
byte[] datalen = new byte[4];
datalen = Arrays.copyOfRange(head, 6, 10);
int nDatalen = ByteConvertUtils.bytesToInt(datalen);
Log.e(TAG, "type:" + nType);
switch (nType) {// App登陸/查詢網關
case Constants.VEDIO_STREAM:
byte[] data = null;
if (nDatalen > 0) {
// 接收指定長度的內容
data = new byte[nDatalen];
dis.readFully(data, 0, nDatalen);
Log.e(TAG, "len:" + nDatalen);
sendMessage(data);
}
break;
default:
Log.e(TAG, "---->不識別的命令");
break;
}
};
public void sendMessage(byte[] data) {
Message msg = hander.obtainMessage();
msg.what = 0;
msg.obj = data;
hander.sendMessage(msg);
}
@Override
public void onCreate() {
Log.e(TAG, "---->" + "onCreateservice");
super.onCreate();
}
@Override
public void onDestroy() {
Log.e(TAG, "---->service停止");
super.onDestroy();
isAccepte = false;
if (readThread != null && readThread.isAlive()) {
readThread.interrupt();
readThread = null;
}
initSocketThread.interrupt();
initSocketThread = null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e(TAG, "---->" + "onStartCommand");
return super.onStartCommand(intent, flags, startId);
}
public class MyBinder extends Binder {
public BindService getService() {
return BindService.this;
}
}
public MyBinder mBinder = new MyBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
主要用來和視頻采集端進行socket通信。
另外附上一個簡單的靜態類
package com.fm.constants;
public class Constants {
public static final int APP_CTL_HEAD = 3000;// 包頭標識
public static final byte[] APPREQUEST_HEAD={0x0b,(byte) 0xB8};//包頭標識
public static final int VEDIO_STREAM = 9800;//服務端將要發送播放視頻命令
}
一、需要下載安裝的東西1. 文件下載網上也有挺多安裝教程的,這裡我提供我的安裝方法。Win10 64位。一些文件我在後面打包。2016.9.12號本人安裝記錄。SDK:
需求:想讓用戶掃描一個二維碼就能下載APP,並統計被掃描次數。兩種實現方法:1.一般我們用草料生成二維碼,如果沒有注冊的話只能生成一個包含下載網址的靜態碼,沒有統計功能,
側滑菜單在很多應用中都會見到,最近QQ5.0側滑還玩了點花樣~~對於側滑菜單,一般大家都會自定義ViewGroup,然後隱藏菜單欄,當手指滑動時,通過Scr
前言在決定用這個標題之前甚是忐忑,主要是擔心自己對AIDL的理解不夠深入,到時候大家看了之後說——你這是什麼玩意兒,就這麼點東西就敢說夠了?簡直是