編輯:關於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);
List previewSizes = 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;//服務端將要發送播放視頻命令
}
[Android測試] Android Studio+Appium+Java+Windows 自動化測試之二:Appium環境安裝搭建
一、需要下載安裝的東西1. 文件下載網上也有挺多安裝教程的,這裡我提供我的安裝方法。Win10 64位。一些文件我在後面打包。2016.9.12號本人安裝記錄。SDK:
掃二維碼下載apk並統計被掃描次數
需求:想讓用戶掃描一個二維碼就能下載APP,並統計被掃描次數。兩種實現方法:1.一般我們用草料生成二維碼,如果沒有注冊的話只能生成一個包含下載網址的靜態碼,沒有統計功能,
Android 自定義控件打造史上最簡單的側滑菜單
側滑菜單在很多應用中都會見到,最近QQ5.0側滑還玩了點花樣~~對於側滑菜單,一般大家都會自定義ViewGroup,然後隱藏菜單欄,當手指滑動時,通過Scr
Android:學習AIDL(上)
前言在決定用這個標題之前甚是忐忑,主要是擔心自己對AIDL的理解不夠深入,到時候大家看了之後說——你這是什麼玩意兒,就這麼點東西就敢說夠了?簡直是