寶可夢已經重大更新,多了團戰,並翻新道館,去年夏天的感動已不在了,心裏感傷,人們總是缺乏毅力,一款能出外運動段練身心的好手遊,人們己經忘記了,雖然如此,自己假日一樣打開寶可夢,去外面捕捉可愛的寶可夢,吹吹風,欣賞外面的風景,當作運動,寫這文章是要讓自己記得,勉勵自己。
read more...
2017年5月17日 星期三
透過MediaExtractor MediaCodec MediaMuxer作音視頻後製處理,影片加上時間
未處理的音視頻
處理音視頻加上時間
前言,對MediaCodec有一番認識後,試著對音視頻作後製處理,我試著對影片加上時間,再網路上搜尋一堆資料,有初步體認後,就動手試寫了,沒想到工程滿浩大的,還有很多坑,最後做出來了,但不是每部手機都能跑得完整,HTC Desire820跑OK,HTC e9 plus無法用codec.getOutputImage(index)取得一偵圖片,用ByteBuffer buffer = codec.getOutputBuffer(index)來處理,但是視頻有殘影,原因是在轉RGB時再轉回YUV420沒轉好,找不到方法,只能先這樣,實際上COLOR_FormatYUV420Flexible格式ByteBuffer就是YUV420,用Image image = codec.getOutputImage(index)還得多轉成YUV420位元組(byte[]),HTC Desire820用getInputBuffer來處理視頻花掉(應該只是轉換就會好,但是找不到資料先不管),用getOutputImager來處理就OK
首先是圖片繪製觀念,要在每一偵圖片作Canvas繪製,前提圖片需是BMP,解碼器視頻抽出一偵圖片Image image = codec.getOutputImage(index)是YUV420所以要轉到BMP,因為MediaCodec format需是COLOR_FormatYUV420Flexible格式,因此要轉成RGB後,壓成BMP才能用Canvas繪製,繪製完再轉回YUV420,才能餵給編碼器,讓合成器作音視頻合成,這轉換時間太耗時了,我用HTC Desire820處理一分鐘影片快要一個鐘頭,轉換流程如下,data是image轉成的YUV420位元組(byte[]),網路上有轉換程式(我列在下面的getDataFromImage就是轉換程式),然後用YuvImage壓成流,再decode成bmp,我們就可用canvas來作畫,這裡bmp添加時間,添完時間,就要再轉回YUV420這樣byte[]就可餵給MediaCodec,getNV21是轉換程式,網路上也找的到(我改寫列在下面),這裡強調的是流程,全部程式太多了就不全列出來,只列出轉換的程式
//data YUV420轉rgb,作渲染
ByteArrayOutputStream out = new ByteArrayOutputStream();
Rect rect = new Rect(0, 0, width, height);
//YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, rect.width(), rect.height(), null);
YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, rect.width(), rect.height(), null);
yuvImage.compressToJpeg(rect, 100, out);
//RGB data
byte[] imagebytes = out.toByteArray();
Bitmap bmp = BitmapFactory.decodeByteArray(imagebytes, 0, imagebytes.length).copy(Bitmap.Config.ARGB_8888, true);
Paint paint = new Paint();
paint.setColor(Color.GREEN);
Canvas canvas = new Canvas(bmp);
paint.setStrokeWidth(15);
paint.setTextSize(50);
canvas.drawText(get_time(), 100, 150, paint);
//Log.d("TAG_T",""+get_time());
//canvas.drawText(videoTimeFormat.format(info.presentationTimeUs / 1000), 100, 150, paint);
//b=true save file
if (IS_SAVE) {compressToJpeg(i,bmp);}
//再轉回YUV420這個可給MediaCodec
byte[] encode_data = getNV21(width, height, bmp);
下面是image YUV420轉成 YUV420格式為NV21或I420 byte array(這是google找到的,這裡第2參數必需用colorFormat=COLOR_FormatNV21),Image image = codec.getOutputImage(index)就需透過下面轉成YUV 420 byte array,才能轉到RGB
////////////////////////////////////////YUV 420 byte array//////////////////////////////////////////////////////
private byte[] getDataFromImage(Image image, int colorFormat) {
if (colorFormat != COLOR_FormatI420 && colorFormat != COLOR_FormatNV21) {
throw new IllegalArgumentException("only support COLOR_FormatI420 " + "and COLOR_FormatNV21");
}
if (!isImageFormatSupported(image)) {
throw new RuntimeException("can't convert Image to byte array, format " + image.getFormat());
}
Rect crop = image.getCropRect();
int format = image.getFormat();
int width = crop.width();
int height = crop.height();
Image.Plane[] planes = image.getPlanes();
byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
byte[] rowData = new byte[planes[0].getRowStride()];
if (VERBOSE)Log.d("TAG_L", "get data from " + planes.length + " planes");
int channelOffset = 0;
int outputStride = 1;
for (int i = 0; i < planes.length; i++) {
switch (i) {
case 0:
channelOffset = 0;
outputStride = 1;
break;
case 1:
if (colorFormat == COLOR_FormatI420) {
channelOffset = width * height;
outputStride = 1;
} else if (colorFormat == COLOR_FormatNV21) {
channelOffset = width * height + 1;
outputStride = 2;
}
break;
case 2:
if (colorFormat == COLOR_FormatI420) {
channelOffset = (int) (width * height * 1.25);
outputStride = 1;
} else if (colorFormat == COLOR_FormatNV21) {
channelOffset = width * height;
outputStride = 2;
}
break;
}
ByteBuffer buffer = planes[i].getBuffer();
int rowStride = planes[i].getRowStride();
int pixelStride = planes[i].getPixelStride();
if (VERBOSE) {
Log.v(TAG, "pixelStride " + pixelStride);
Log.v(TAG, "rowStride " + rowStride);
Log.v(TAG, "width " + width);
Log.v(TAG, "height " + height);
Log.v(TAG, "buffer size " + buffer.remaining());
}
//if i==0 fhift=0 else shift=1
int shift = (i == 0) ? 0 : 1;
int w = width >> shift;
int h = height >> shift;
buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
for (int row = 0; row < h; row++) {
int length;
if (pixelStride == 1 && outputStride == 1) {
length = w;
buffer.get(data, channelOffset, length);
channelOffset += length;
} else {
length = (w - 1) * pixelStride + 1;
buffer.get(rowData, 0, length);
for (int col = 0; col < w; col++) {
data[channelOffset] = rowData[col * pixelStride];
channelOffset += outputStride;
}
}
if (row < h - 1) {
buffer.position(buffer.position() + rowStride - length);
}
}
if (VERBOSE) Log.v(TAG, "Finished reading data from plane " + i);
}
return data;
}
下面是bmp轉回YUV420 byte array,也是google找到的,不過跟原本程式不一樣了,我改寫for迴圈內只用weight和height處理每一點像素,這樣才能用Thread,可開啟多個Thread來處理,split=2就是把height切成2段,會開啟2個Thread,第1個Thread處理0到height/2,第2個Thread處理height/2到height,並Thead加入List,當List中所有Thread都執行完畢,才會往下走,split=32,就會開啟32個Thread,本來以為開啟越多Thread會快很多,但我錯了,CPU不快在多Thread也沒用,我這選split=2,還有特別注意一點下面的B=Color.red(argb[num]),R=Color.blue(argb[num]),B應該是blue,R是red才對,這不是寫錯了,是BMP的B跟R是相反的,一開始不知道有這個坑,跑出來顏色一直不正確,在google找好久,找到一篇說BMP的B跟R是相反的,我將B和R對調後顏色就正確了.
public byte[] getNV21(int width, int height, Bitmap scaled) {
int[] argb = new int[width * height];
scaled.getPixels(argb, 0, width, 0, 0, width, height);
byte[] yuv = new byte[width * height * 3 / 2];
encodeYUV420SP(yuv, argb, width, height);
scaled.recycle();
return yuv;
}
public void encodeYUV420SP(byte[] yuv420sp, int[] argb, int width, int height) {
final int frameSize = width * height;
List<caulate> th_list=new ArrayList<>();
//int yIndex = 0;
//int uvIndex = frameSize;
//數字越大,啟用的thread越多,越多thread不一定越快
int split=2;//2的N次方
int dh=height/split;
//使用多個thread處理YUV420SP
for (int i=0;i<height;i=i+dh ){
int end=i+dh;
if(end>height){end=height;}
caulate ac=new caulate(yuv420sp, argb, width, height,i,end);
th_list.add(ac);
}
//當所有thread都結束
for(int j=0;j<th_list.size();j++){
while (th_list.get(j).isAlive()){}
}
}
private class caulate extends Thread{
int[] argb;
byte[] yuv420sp;
int width;
int height;
int jbegin;
int jend;
public caulate(byte[] myuv420sp, int[] margb, int mwidth, int mheight,int mjbegin,int mjend){
yuv420sp=myuv420sp;
argb=margb;
width=mwidth;
height=mheight;
jend=mjend;
jbegin=mjbegin;
this.start();
}
@Override
public void run(){
final int frameSize = width * height;
//int yIndex = 0;
//int uvIndex = frameSize;
int a, R, G, B, Y, U, V;
//int index = 0;
for (int j =jbegin ; j < jend; j++) {
for (int i = 0; i < width; i++) {
int num=i+j*width;
//BMP是BGR 我們把R,B對調
//a = (argb[index] & 0xff000000) >> 24; // a is not used obviously
//R = (argb[index] & 0xff0000) >> 16;
//G = (argb[index] & 0xff00) >> 8;
//B = (argb[index] & 0xff) >> 0;
B=Color.red(argb[num]);
G=Color.green(argb[num]);
R=Color.blue(argb[num]);
// well known RGB to YUV algorithm
Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;
// NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2
// meaning for every 4 Y pixels there are 1 V and 1 U. Note the sampling is every other
// pixel AND every other scanline.
yuv420sp[num] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
if (j % 2 == 0 && (num) % 2 == 0) {
//if v<0 等於0,否則V>255 等於255否則等與V
// yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
// yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
int unum=i+frameSize;
int u_num=unum+j*width/2;
int u_num1=u_num+1;
//if v<0 等於0,否則V>255 V等於255否則等與V
yuv420sp[u_num] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
yuv420sp[u_num1] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
//Log.d("TAG_CA","j :" +j);
//Log.d("TAG_CA","u_num :" +u_num1);
//Log.d("TAG_CA","uvIndex :" +uvIndex);
//try{Thread.sleep(50);}catch(Exception e){}
}
//index++;
}
}
}
}
再來說音視頻解碼加編碼,讓視頻跟音頻分離要用MediaExtractor,視頻分離後從MediaExtractor readSampleDataget寫入InputBuffer,queueInputBuffer讓解碼器抽出每一偵getOutputBuffer(或getOutputImage),作Canvas繪製就可丟給編碼器,然後releaseOutputBuffer,編碼器編完碼再用MediaMuxer作合成,總共要2個MediaExtractor(一個也行作音視頻分離),4個MediaCodec(video解碼,video編碼,audio解碼,audio編碼),一個MediaMuxer(作音視頻合成)
整個流程
視頻 -> 解碼(decode) ->繪製處理 - 編碼(encode)
MediaExtractor( 音視頻分離 )-> ->MediaMuxer(作音視頻合成)
音頻 -> 解碼(decode) -> 編碼(encode)
視頻解碼(decode)流程
readSampleDataget(extracter)-> InputBuffer->queueInputBuffer解碼(decode)->getOutputBuffer->canvas繪製處理(處理後可給編碼器)->releaseOutputBuffer
音頻解碼(decode)流程
readSampleDataget(extracter)-> InputBuffer->queueInputBuffer解碼(decode)->getOutputBuffer(buffer資料可給編碼器)->releaseOutputBuffer
視頻編碼(decode)流程
canvas繪製處理(處理後可給編碼器)-> InputBuffer->queueInputBuffer編碼(encode)->getOutputBuffer(buffer寫入合成器)->releaseOutputBuffer
音頻編碼(decode)流程
(buffer資料可給編碼器)-> InputBuffer->queueInputBuffer編碼(encode)->getOutputBuffer(buffer寫入合成器)->releaseOutputBuffer
一開始4個MediaCodec都是用異步處理,在音頻編碼時,聲音異常,一邊跑程式我用AudioTrack來play聲音測試,在解碼這部份過程都OK,但play放到編碼的InputBuffer這段後,聲音測試就不對了,猜想,因為解碼跟編碼不是同步,會有問題,有可能解碼丟2筆,可是編碼再第一或第三筆資料,因為異步處理一個Mediacodec就是一個Thread,要解這問題,可用儲列方法,解碼放到一個List,編碼再一筆一筆拿出來,不過我沒試這方法,我將音頻編碼改用同步處理且不用Thread去處裡,這樣音頻問題就解掉了,寫這程式遇到好多陷阱跟問題.
read more...
2017年5月3日 星期三
錄製手機螢幕聲音MediaCodec異步處理
MediaCodec異步處理,之前那方法是同步處理,之前文章我說錯了,寫入影音文件應該說編碼器才對,處理讀取影音文件才是解碼器,編碼解碼原理都一樣,
試著將錄取手機螢幕影像,之前程式改成MediaCodec異步處理,異步處理比同步處理更為直覺,寫法比較清楚,比較符合物件導向,我比較喜歡這樣的寫法.
說一下MediaCodec異步處理寫法原理,我喜歡用extends然後覆寫其中方法,總覺得這樣寫法才優雅,我們首先extends MediaCodec.Callback,然後覆寫其中4個方法Method(如下面),在啟動MediaCodec前MediaCodec.setCallback(this)設定回傳Class為this,MediaCodec.start()開始後,這4個覆寫方法,就會開始時時監聽MediaCodec狀態,
1. onInputBufferAvailable監聽輸入端buffer可獲取,
2. onOutputBufferAvailable監聽輸出端buffer可獲取,
3. onOutputFormatChanged監聽格式發生變化,
4. onError監聽發生錯誤,
當然在這之前要先格式化MediaCodec,準備好代為處理輸入端的VirtualDisplay,方法中codec參數和index參數,info參數,就是監聽MediaCodec後的數據值,用這些參數來處理我們想要的資料,codec參數指該MediaCodec,index參數指MediaCodec索引值,info參數指MediaCodec buffer資訊,參數format指MediaCodec 格式發生變化format(onOutputFormatChange)
譬如codec.getOutputBuffer(index)就能取得輸出buffer,info.size()取得buffer size值
//1 監聽輸入端buffer可獲取
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
//codec索引值index,處理codec
//這裡沒用到,Virtualdisplay已取代輸入端
}
//2 監聽輸出端buffer可獲取
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
Log.d("TAG","Output buffer begin.");
ByteBuffer encodeData = codec.getOutputBuffer(index);
........................................................
codec.releaseOutputBuffer(index, false);//觸理完一回,一定要釋放輸出buffer
}
//3 監聽codec發生錯誤重設codec
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
Log.d("TAG","Error:"+e);
codec.reset();
}
//4 監聽格式發生變化重設格式,啟動合成器MediaMuxer
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
Log.d("TAG","output format already changed!");
//vedio resetOutputFormat
//MediaFormat newFormat = codec.getOutputFormat();
//MediaMuxer addtrack
mVideoTrackIndex = mMediaMuxer.addTrack(format);
mMediaMuxer.start();
}
關鍵碼,部份拿掉,這樣流程較清楚
public class surface_mediacodec_advance extends MediaCodec.Callback{
//private Boolean MUXER_START = false;
private MediaProjection mp;
private AtomicBoolean mQuit = new AtomicBoolean(false);
private int mscreendi;
private VirtualDisplay vp;
private String save_path = "/sdcard/capture.mp4";
private check_file_save_finish mcheck_file_save_finish;
private long lastpresentationTimeUs;
private int VIDEO_KEY_BIT_RATE = 4000000;//2000000 4000000
private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding
private static final int FRAME_RATE = 30; // 30 fps
private static final int IFRAME_INTERVAL = 10; // 10 seconds between I-frames
private static final int TIMEOUT_US = 10000;
private int CH_TYPE;
//使用者已許可MediaProjection mp
public surface_mediacodec_advance(int windowWidth, int windowHeight, int sdi, MediaProjection m, int audio_sample_rate, int ch) {
CH_TYPE=ch;
//初始化MediaCodec
mwindowWidth = windowWidth;
mwindowHeight = windowHeight;
mp = m;
mscreendi = sdi;
try {
mMediaMuxer= new MediaMuxer(save_path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
} catch (IOException e) {
throw new RuntimeException(e);
}
Log.d("TAG"," CH TYPE :"+CH_TYPE);
}
//開始錄製
public final void START_RECORDER() {
preparerecorder_other();//設置MediaCodec video格式
//preapareaudioencoder();//設置MediaCodec audio
vp = mp.createVirtualDisplay("record_screen",
mwindowWidth, mwindowHeight, mscreendi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mSurface, null, null);
}
//停止錄製
public final void STOP_RECORDER() {
release();//停止要release
}
//設置mMediaCodec格式
private void preparerecorder_other() {
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mwindowWidth, mwindowHeight);
//format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
//MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_KEY_BIT_RATE);
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
Log.d("TAG", "created video format: " + format);
try {
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
} catch (IOException e) {
}
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mSurface = mMediaCodec.createInputSurface();
Log.d("TAG", "created input surface: " + mSurface);
//設定MediaCodec回傳Class
mMediaCodec.setCallback(this);
mMediaCodec.start();
}
//release Mediacodec這順序要對否則格式會錯
private void release() {
mcheck_file_save_finish=new check_file_save_finish(save_path);
while (!mcheck_file_save_finish.check_file_save_finish()){
Log.d("TAG_NO","NO FINISH");
}
if (vp != null) {
vp.release();
}
if (mMediaMuxer != null) {
mMediaMuxer.stop();
mMediaMuxer.release();
mMediaMuxer = null;
}
//
if (mMediaCodec != null) {
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec= null;
}
//監聽輸入buffer可獲取
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
//這裡沒用到,Virtualdisplay已取代輸入端
}
//監聽輸出buffer可獲取
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
Log.d("TAG","Output buffer begin.");
ByteBuffer encodeData = codec.getOutputBuffer(index);
if ((info.flags & codec.BUFFER_FLAG_CODEC_CONFIG) != 0) {//編碼數據,非媒體數據,忽略它
info.size = 0;
}
if (info.size == 0) {//編碼數據,非媒體數據,忽略它
encodeData = null;
Log.d("TAG", "Ignore Data");
} else { //非媒體數據,產生提示
Log.d("TAG", "Get Info: size="
+ info.size + " Video presentationTimeUs="
+ info.presentationTimeUs + " offset="
+ info.offset);
}
if (encodeData != null) { //媒體數據寫入
encodeData.position(info.offset);
encodeData.limit(info.offset + info.size);
mMediaMuxer.writeSampleData(mVideoTrackIndex, encodeData, info);
Log.d("TAG", "Send Info: size=" + info.size);
}
codec.releaseOutputBuffer(index, false);//釋放輸出buffer
}
//codec發生錯誤重設codec
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
Log.d("TAG","Error:"+e);
codec.reset();
}
//格式發生變化重設格式
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
Log.d("TAG","output format already changed!");
//vedio resetOutputFormat
//MediaFormat newFormat = codec.getOutputFormat();
//MediaMuxer addtrack
mVideoTrackIndex = mMediaMuxer.addTrack(format);
mMediaMuxer.start();
}
}
據上面錄製手機改影像編碼器為異步處理後,接著又完成了MediaCodec異步處理音頻編碼,音頻編碼異步流程,同影像編碼只是多處理輸入端
監聽輸入onInputBufferAvailable中去讀取MIC麥克風buffer資料,getInputBuffer(index)取得MediaCodec輸入buffer,將資料填入這buffer,然後queueInputBuffer(),此時onOutputBufferAvailable監聽輸出buffer可獲取,透過界面送到影像編碼寫入合成器,然後釋放此buffer,然後監聽輸入onInputBufferAvailable變又可獲取,重複上面,又去讀取MIC麥克風buffer資料.................
音頻編碼器,設計一個界面capture_audio_outpu_buffer,含實作2個方法
//捕捉輸出buffer資料,以便寫入合成器
void on_capture_audio_outpu_buffer(MediaCodec codec,ByteBuffer encodedAudioData,MediaCodec.BufferInfo info);
//audio MediaCodec初始化MediaFormat,以便產生AudioTrackIndex,使能夠啟動合成
void on_capture_mediaformat(MediaFormat mMediaFormat);
影像編碼器,實作上面界面的2個方法,影像編碼器在監聽onOutputFormatChanged中實例化音頻編碼器,set_capture_audio_outpu_buffer_listener監聽this,start()後on_capture_mediaformat會產生AudioTrackIndex,on_capture_audio_outpu_buffer開始監聽音頻編碼器的輸出資料,寫入合成器,並啟動合成器
後記:遇到有些手機AudioRecoderformat 設置立體聲,聲道數不同錄製完,回放影片聲音會變快,看一下程式就知道,是因為Audio取樣時,buffer length比getInputBuffer(index)的剩餘長度,還長,沒完全填入,只要分次全部填入就解掉了
影像編碼
public class surface_mediacodec_advance extends MediaCodec.Callback implements audio_encoder_advance.capture_audio_outpu_buffer{
.........................................
///////////////////實作 audio_encoder_advance.capture_audio_outpu_buffer Method///////////////////
//監聽audio_encoder_advance MediaFormat初始化一次
@Override
public void on_capture_mediaformat(MediaFormat mMediaFormat){
//產生AudioTrackIndex
mAudioTrackIndex = mMediaMuxer.addTrack(mMediaFormat);
}
//時時監聽audio_encoder_advance outpu buffer
@Override
public void on_capture_audio_outpu_buffer(MediaCodec codec,ByteBuffer encodedAudioData,MediaCodec.BufferInfo info){
//audio_encoder_advance輸出,寫入合成器
mMediaMuxer.writeSampleData(mAudioTrackIndex, encodedAudioData, info);
}
........................................................................................
//格式發生變化實例化音頻編碼器,set_capture_audio_outpu_buffer_listener,start()後會產生AudioTrackIndex,
//開始監聽audio_encoder_advance outpu buffer寫入合成器
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
Log.d("TAG_C","output format already changed!");
//vedio resetOutputFormat
//MediaFormat newFormat = codec.getOutputFormat();
//MediaMuxer addtrack
mVideoTrackIndex = mMediaMuxer.addTrack(format);
//這要通知其他mediacodec reset
if(maudio_encoder_advance==null){
Log.d("TAG"," SAMPLE RATE AUDO :"+maudio_sample_rate);
//實例化audio_encoder_advance
maudio_encoder_advance=new audio_encoder_advance(maudio_sample_rate,CH_TYPE);
//設定傾聽事件
maudio_encoder_advance.set_capture_audio_outpu_buffer_listener(this);
}
//audio_encoder_advance開始編碼
maudio_encoder_advance.start();
//若存在,resetOutputFormat,要在這reset其他format
//if(maudio_encoder_advance!=null){
// maudio_encoder_advance.resetOutputFormat();
//}
//啟動合成器
mMediaMuxer.start();
}
...............................................
}
音頻編碼
audio_encoder_advance class
public class audio_encoder_advance extends MediaCodec.Callback{
public void start(){
preapareaudioencoder
}
private void preapareaudioencoder() {
//設置audio MediaCodec格式
.............................
if(mlistener!=null) {
//設新newAudioFormat給監聽者,以便產生AudioTrackIndex
mlistener.on_capture_mediaformat(newAudioFormat);
}
//設回call
mAudioEncoder.setCallback(this);
mAudioEncoder.start();
}
....................................................................................
//時時監聽輸出buffer可獲取
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
...............................................................................
if(mlistener!=null) {
//設輸出資料給監聽者
mlistener.on_capture_audio_outpu_buffer(codec, encodedAudioData, info);
}
}
/////////////////////////////////界面////////////////////////////////////////////////////////////////
//interface變數
private capture_audio_outpu_buffer mlistener=null;
//設定listener
public void set_capture_audio_outpu_buffer_listener(capture_audio_outpu_buffer ca){
mlistener=ca;
}
//設計界面
public interface capture_audio_outpu_buffer{
//捕捉輸出buffer資料
void on_capture_audio_outpu_buffer(MediaCodec codec,ByteBuffer encodedAudioData,MediaCodec.BufferInfo info);
//audio MediaCodec初始化MediaFormat
void on_capture_mediaformat(MediaFormat mMediaFormat);
}
}
read more...
2017年5月1日 星期一
Android 6.0以後一般權限,用一個class輕鬆完成
Android 6.0以後一般權限不只要在AndroidManifest.xml聲明,Activity class還要實作onRequestPermissionsResult(....)給使用者授權
,每個App都有自己管理權限的頁面如影片,給使用者授權,一開始要判斷該項權限是否授權,每授權就詢問是否要求授權,否則應用程式會崩潰
第一個requestCode引數,給各任何數字check既可(如CAPTURE_AUDIO=9999),第2引數不曉得作用,反正沒用到,第3個引數grantResults,是指授權項,若有2個權限要授權會是grantResults[0],grantResults[1],下面用grantResults.length取得項目數用for去逐一判斷,是否全授權,是就運行程式,否就不能運行程式,再去要求授權.
check_permission.newInstance(this)特別用一個Class去處裡授權項
ContextCompat.checkSelfPermission(mActivity, sti)==PackageManager.PERMISSION_DENIED來判斷該項是否有授權,若沒授權加入一個List,sti是指android.Manifest.permission.XXXXX,
若有沒授權ActivityCompat.requestPermissions(mActivity, get_str, CAPTURE_AUDIO)要求授權,get_str是指沒授權的String List
A.
設定檢查一般權限項目在str_permission設定既可如下,若是只要CAMERA,只要設str_permission={"CAMERA"}要跟AndroidManifest.xml也要加上
private final String[] str_permission={
"CAMERA",
"RECORD_AUDIO",
"WRITE_EXTERNAL_STORAGE",
"READ_PHONE_STATE"
};
B.
若有其他權限要加,根據需要加上需要的一般permission
若知道所有一般permission乾脆全部加上也可,省得麻煩
public String get_permission_string(String st){
String s=null;
switch(st){
case "RECORD_AUDIO":
s=android.Manifest.permission.RECORD_AUDIO;
break;
case "WRITE_EXTERNAL_STORAGE":
s=android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
break;
case "READ_PHONE_STATE":
s= Manifest.permission.READ_PHONE_STATE;
break;
case "CAMERA":
s= Manifest.permission.CAMERA;
break;
//在這加入一般permission case
}
return s;
}
這個check_permission class可當作API來使用,任何App都可使用
MainActivity只需呼叫check_permission.newInstance(this);就會檢查str_permission設定的項目,
AndroidManifest.xml
...........................................................................
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
............................................................................
MainActivity
public class MainActivity extends AppCompatActivity{
..........................................................
private int CAPTURE_AUDIO=9999;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(Build.VERSION.SDK_INT>=23) {
//檢查是否有授權沒授權b=true
check_permission.newInstance(this);
}
}
@Override
public void onRequestPermissionsResult(int requestCode,String[] permission,int[] grantResults){
super.onRequestPermissionsResult(requestCode,permission,grantResults);
if(requestCode==CAPTURE_AUDIO){
//先預設都通過授權
boolean b=true;
for(int i=0;i<grantResults.length;i++){
//逐一檢查,若一項沒通過,就不通過,
if(grantResults[i]!= PackageManager.PERMISSION_GRANTED){b=false;}
}
if(b){
Toast.makeText(this,"OK", Toast.LENGTH_LONG).show();
START_VISUALIZER=true;
}else{
START_VISUALIZER=false;
Toast.makeText(this,"Cancle", Toast.LENGTH_LONG).show();
}
}
}
......................................................................
}
check_permission類,下面使用靜態static, newInstance來new class,如check_permission.newInstance(this)
這樣就會實例化一個check_permission,注意方法中會實例化check_permission類,並返回
public static check_permission newInstance(MainActivity obj){
mActivity=obj;
check_permission mcheck_permission=new check_permission();
return mcheck_permission;
}
check_permission
package com.example.yplin.test_audio;
/**
* 檢查授權
* 1.
* 需要可添加在get_permission_string()權限中
* 2.
* String[] str_permission設定檢查一般權限項目
* 3.
* Activity呼叫 check_permission.newInstance(myActivity)開始check
*/
import android.Manifest;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Created by yplin on 2017/4/26.
*/
public class check_permission {
private static Activity mActivity=null;
//設定檢查一般權限項目,若是只要CAMERA,只要設str_permission={"CAMERA"}
private final String[] str_permission={
"CAMERA",
"RECORD_AUDIO",
"WRITE_EXTERNAL_STORAGE",
"READ_PHONE_STATE"
};
private int CAPTURE_AUDIO=9999;
public check_permission(){
run_check(str_permission);
}
public static check_permission newInstance(MainActivity obj){
mActivity=obj;
check_permission mcheck_permission=new check_permission();
return mcheck_permission;
}
public void run_check(String[] st){
//不能用null
List<String> list=new ArrayList();
int k=0;
for(int i=0;i<st.length;i++)
{
//一個個判斷
String sti=get_permission_string(st[i]);
boolean b= ContextCompat.checkSelfPermission(mActivity, sti)==PackageManager.PERMISSION_DENIED;
//把沒取得的Permission加入list
if(b){
Log.d("TAG",sti);
list.add(k,sti);
k++;
}
}
//String[]一定要給正確length,根據list.size()給正確length
String[] get_str=new String[list.size()];
//重新產生需要求permission String[] get_str
for(int j=0;j<list.size();j++){
Log.d("TAG",list.get(j));
get_str[j]=list.get(j);
}
//若沒有授權項目,要求授權,0就已全授權忽略
if(list.size()>0){
ActivityCompat.requestPermissions(mActivity, get_str, CAPTURE_AUDIO);
}
}
//根據需要加上需要的一般permission
public String get_permission_string(String st){
String s=null;
switch(st){
case "RECORD_AUDIO":
s=android.Manifest.permission.RECORD_AUDIO;
break;
case "WRITE_EXTERNAL_STORAGE":
s=android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
break;
case "READ_PHONE_STATE":
s= Manifest.permission.READ_PHONE_STATE;
break;
case "CAMERA":
s= Manifest.permission.CAMERA;
break;
//在這加入一般permission case
}
return s;
}
}
read more...
2017年4月30日 星期日
JAVA界面,如何設計界面含傾聽事件
常常用到實作Java API已寫好的界面,可是都從沒實際設計界面含傾聽事件,現在就來說要如何弄出可時時發送資料的界面,讓其他類監聽,取得資料
下面程式是大致的流程,interface_A類,設計界面data_change_lister被監聽類實作,含有一方法void capture_data_change(int data)要被監聽類實作的方法,其中data是我們要被其他監聽類,監聽的資料,void set_data_change_lister(data_change_lister lister)這個可指定其他監聽類來傾聽
,透過mlister.capture_data_change(i)來發送資料.
當然要達到像下面不同類傳送資料,不用界面也可,但都要在類內實例化其他監聽類,在送出資料,程式碼顯得很亂,透過界面不須實例化監聽類,界面類根本不需要考慮是任何類來監聽資料,變得有規律,透過界面更為周詳,更有彈性,譬如在遊戲中做幾拾個甚至幾百個物體碰撞判別,之前文章python gtk cairo射擊小遊戲,也有類似方法,不過有些時間忘了是如何實現的,要回去看程式碼才知道.
簡單說java 的界面類,不需實例化監聽類,只需指定界面變數,透過界面實例化該監聽類成員,不需要管監聽成員的類別,若不用界面也可以,但實例化必須是該監聽類,才能對該監聽類發送資料,以前我都是用後者來發送.
注意下面紅色粗體字mlister是界面變數,但在set_data_change_lister,卻是可以指定任何類成為這個界面變數,原因在監聽類implements後就可變成該界面型態,而被當引數指成改界面變數mlister,呼叫本身的實作方法mlister.capture_data_change(i),就可取得該i資料了,若界面類是一個Thread,將傳送資料放在run()方法中while(){傳送資料},start()後可持續監聽該資料
主Class 監聽類B:
Class B宣告實作interface_A.data_change_lister,實例化interface_A,加入事件傾聽者minterface_A.set_data_change_lister(this)
實作capture_data_change(int data)監聽了data,start thread結果會在terminal印出0 1 2 3 4 5 6 7 8 9
//Class B宣告實作interface_A.data_change_lister
public class B implements interface_A.data_change_lister{
public B(){
//實例化interface_A
interface_A minterface_A=new interface_A();
//加入事件傾聽者
minterface_A.set_data_change_lister(this);
//start thread
minterface_Aminterface_A.start()
}
@Override
public void capture_data_change(int data){
//時時傾聽data變化
Log.d("TAG_DATA","Data change: "+data);
}
}
界面類interface_A:注意這裡沒實例化CLass B,是透過界面來實例化任何監聽類
//Class interface_A設計界面data_change_lister
public class interface_A extends Thread{
//這裡指定界面變數,用來接受監聽類成員
private data_change_lister mlister;
//建構子
public interface_A(){..........}
@Overrid
public void run(){
//舉例傳送0 1 2 3 4 5 6 7 8 9資料
if(mlister!=null)
{
//透過介面mlister變數給事件傾聽者傳送資料
for(int i=0;i < 10;i++){
mlister.capture_data_change(i);
}
}
}
//關鍵在這界面變數,可以設定是任何類,設定mlister變數成為其他監聽類實例化
public void set_data_change_lister(data_change_lister lister){
mlister=lister;
Log.d("TAG_DATA","set listener "+mlister);
}
//設計data_change_lister界面
public interface data_change_lister{
//資料變化時,捕捉資料data
void capture_data_change(int data);
}
}
read more...
下面程式是大致的流程,interface_A類,設計界面data_change_lister被監聽類實作,含有一方法void capture_data_change(int data)要被監聽類實作的方法,其中data是我們要被其他監聽類,監聽的資料,void set_data_change_lister(data_change_lister lister)這個可指定其他監聽類來傾聽
,透過mlister.capture_data_change(i)來發送資料.
當然要達到像下面不同類傳送資料,不用界面也可,但都要在類內實例化其他監聽類,在送出資料,程式碼顯得很亂,透過界面不須實例化監聽類,界面類根本不需要考慮是任何類來監聽資料,變得有規律,透過界面更為周詳,更有彈性,譬如在遊戲中做幾拾個甚至幾百個物體碰撞判別,之前文章python gtk cairo射擊小遊戲,也有類似方法,不過有些時間忘了是如何實現的,要回去看程式碼才知道.
簡單說java 的界面類,不需實例化監聽類,只需指定界面變數,透過界面實例化該監聽類成員,不需要管監聽成員的類別,若不用界面也可以,但實例化必須是該監聽類,才能對該監聽類發送資料,以前我都是用後者來發送.
注意下面紅色粗體字mlister是界面變數,但在set_data_change_lister,卻是可以指定任何類成為這個界面變數,原因在監聽類implements後就可變成該界面型態,而被當引數指成改界面變數mlister,呼叫本身的實作方法mlister.capture_data_change(i),就可取得該i資料了,若界面類是一個Thread,將傳送資料放在run()方法中while(){傳送資料},start()後可持續監聽該資料
主Class 監聽類B:
Class B宣告實作interface_A.data_change_lister,實例化interface_A,加入事件傾聽者minterface_A.set_data_change_lister(this)
實作capture_data_change(int data)監聽了data,start thread結果會在terminal印出0 1 2 3 4 5 6 7 8 9
//Class B宣告實作interface_A.data_change_lister
public class B implements interface_A.data_change_lister{
public B(){
//實例化interface_A
interface_A minterface_A=new interface_A();
//加入事件傾聽者
minterface_A.set_data_change_lister(this);
//start thread
minterface_Aminterface_A.start()
}
@Override
public void capture_data_change(int data){
//時時傾聽data變化
Log.d("TAG_DATA","Data change: "+data);
}
}
界面類interface_A:注意這裡沒實例化CLass B,是透過界面來實例化任何監聽類
//Class interface_A設計界面data_change_lister
public class interface_A extends Thread{
//這裡指定界面變數,用來接受監聽類成員
private data_change_lister mlister;
//建構子
public interface_A(){..........}
@Overrid
public void run(){
//舉例傳送0 1 2 3 4 5 6 7 8 9資料
if(mlister!=null)
{
//透過介面mlister變數給事件傾聽者傳送資料
for(int i=0;i < 10;i++){
mlister.capture_data_change(i);
}
}
}
//關鍵在這界面變數,可以設定是任何類,設定mlister變數成為其他監聽類實例化
public void set_data_change_lister(data_change_lister lister){
mlister=lister;
Log.d("TAG_DATA","set listener "+mlister);
}
//設計data_change_lister界面
public interface data_change_lister{
//資料變化時,捕捉資料data
void capture_data_change(int data);
}
}
read more...
2017年4月29日 星期六
讓Class View跟一般class Thread一模一樣
如何讓Class View跟一般class Thread一模一樣,當要使用Canvas一定要繼承View才能作畫,就沒法繼承Thread,因為java只能單一繼承
若要View能有Thead功能,只能實作Runnable,但是class實作Runnable是沒有像Thread有start()函式,那要test_vew開始執行續要這樣,
要如下面這要,每次都要new Thread,我覺的這樣太麻煩
//實例化test_view
test_view mtest_view=new test_view(.....);
//mtest_view新的Thread才能start(),
new Thread(mtest_view).start();
Example 1:
public class test_view extends View implements Runnable{
@Override
public void run(){
while(true){
this.update();//update view產生動畫
}
}
@Override
protected void onDraw(Canvas canvas) {
//處理canvas
}
}
我想到一個方法,在test_view 加一個start()函式,寫入new Thread(this).start(),那要test_vew開始執行續就跟Class Thread一模一樣
//實例化test_view
test_view mtest_view=new test_view(.....);
//開始Thread
mtest_view.start();
Example 2:
public class test_view extends View implements Runnable{
public void start(){
new Thread(this).start();
}
@Override
public void run(){
while(true){
this.update();//update view產生動畫
}
}
@Override
protected void onDraw(Canvas canvas) {
//處理canvas
}
}
若要實例化就開始Thread,建構子加入this.start(),這樣在實例化就會開始Thread了
//實例化test_view且開始Thread
test_view mtest_view=new test_view(.....);
Example 3:
public class test_view extends View implements Runnable{
public test_view(Context t){
super(t)
this.start();
}
public void start(){
new Thread(this).start();
}
@Override
public void run(){
while(true){
this.update();//update view產生動畫
}
}
@Override
protected void onDraw(Canvas canvas) {
//處理canvas
}
}
這樣extends View implements Runnable對用view來作動畫或遊戲就很方便,一個view就是一個Thread,每各View就有個別的生命,可產生不同行為
read more...
若要View能有Thead功能,只能實作Runnable,但是class實作Runnable是沒有像Thread有start()函式,那要test_vew開始執行續要這樣,
要如下面這要,每次都要new Thread,我覺的這樣太麻煩
//實例化test_view
test_view mtest_view=new test_view(.....);
//mtest_view新的Thread才能start(),
new Thread(mtest_view).start();
Example 1:
public class test_view extends View implements Runnable{
@Override
public void run(){
while(true){
this.update();//update view產生動畫
}
}
@Override
protected void onDraw(Canvas canvas) {
//處理canvas
}
}
我想到一個方法,在test_view 加一個start()函式,寫入new Thread(this).start(),那要test_vew開始執行續就跟Class Thread一模一樣
//實例化test_view
test_view mtest_view=new test_view(.....);
//開始Thread
mtest_view.start();
Example 2:
public class test_view extends View implements Runnable{
public void start(){
new Thread(this).start();
}
@Override
public void run(){
while(true){
this.update();//update view產生動畫
}
}
@Override
protected void onDraw(Canvas canvas) {
//處理canvas
}
}
若要實例化就開始Thread,建構子加入this.start(),這樣在實例化就會開始Thread了
//實例化test_view且開始Thread
test_view mtest_view=new test_view(.....);
Example 3:
public class test_view extends View implements Runnable{
public test_view(Context t){
super(t)
this.start();
}
public void start(){
new Thread(this).start();
}
@Override
public void run(){
while(true){
this.update();//update view產生動畫
}
}
@Override
protected void onDraw(Canvas canvas) {
//處理canvas
}
}
這樣extends View implements Runnable對用view來作動畫或遊戲就很方便,一個view就是一個Thread,每各View就有個別的生命,可產生不同行為
read more...
2017年4月28日 星期五
Android音頻可視化用Visualizer這個API輕鬆實現
Android音頻可視化,可以用Visualizer這個API輕鬆實現,上面影片,啟動6個Thread取得主Class音頻輸出流作動畫Update,
其實簡單方法的不須用Thread,使用監聽方法onWaveFormDataCapture或onFftDataCapture中去update動畫既可
1.宣告實作implements Visualizer.OnDataCaptureListener界面
2.實作OnDataCaptureListener界面的2個method
@Override
public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) {
//取得音頻輸出流,給Canvas作後續處理
mwaveform=waveform;
}
@Override
public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
}
3.設定監聽實作界面的2個method,取得音頻輸出流
//實例化 Visualizer ,0指Audio output
audioOutput = new Visualizer(0);
//加入傾聽事件,時時監聽實作界面的2個method,便可從實作界面的2個method時時取得音頻輸出流
audioOutput.setDataCaptureListener(this,Visualizer.getMaxCaptureRate(),true,false);
4.對音頻輸出流作可視化處理,運算完用Canvas在View Class繪出圖形,google一下有運算公式
把一些不相干的去掉,代碼簡化,比較看得懂
public class MainActivity extends AppCompatActivity implements Visualizer.OnDataCaptureListener{
private Visualizer audioOutput = null;
private byte[] mwaveform=null;
...................................................
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//實例化 Visualizer ,0指Audio output
audioOutput = new Visualizer(0); // get output audio stream
//第2參數true指onWaveFormDataCapture() run第3參數false指onFftDataCapture no run
audioOutput.setDataCaptureListener(this,Visualizer.getMaxCaptureRate(),true,false);
start_Visualizer();
..................................................
}
@Override
protected void onDestroy() {
super.onDestroy();
stop_Visualizer();
//釋放Visualizer
audioOutput.release();
}
/////////////////////////////////////實作OnDataCaptureListener 1////////////////////////////////////
@Override
public void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate) {
//Log.d("TAG","LENGTH"+waveform.length);
//print byte waveform[0]=-128~128 waveform[1]=-128~128 ....................
//可將 waveform[0]... 放入List,再根據List用Canvas畫出來
//取得音頻輸出流,作後續處理
mwaveform=waveform;
}
////////////////////////////////////////實作OnDataCaptureListener 2////////////////////////////////////
@Override
public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
//開始Visualizer
private void star_Visualizert(){
audioOutput.setEnabled(true);
}
//停止Visualizer
private void stop_Visualizer() {
audioOutput.setEnabled(false);
}
//CLASS取得音頻輸出流 method,給Class View Canvas作後續處理
public byte[] get_waveform_value(){
return mwaveform;
}
}
read more...
訂閱:
文章 (Atom)