顯示具有 Android 標籤的文章。 顯示所有文章
顯示具有 Android 標籤的文章。 顯示所有文章

2015年3月31日 星期二

[Android] 使用AudioTrack()會何聲音會播播停停?

使用AudioTrack()時,如果聲音有點過尖或過低
那就可能是格式設定有錯,或者sample長度給的不對,

但如果是播播停停,尤其是遇到類似以下的錯誤訊息
W/AudioTrack(..): releaseBuffer() track 0x6eaf04d0 name=0x1 disabled, restarting
該怎麼處理呢?

會遇到上面的訊息,表示資料給的太慢,
如果是local的檔案,通常不會有問題,
但如果是網路streaming,因為資料來的速率不一,
有時可能會有buffer underflow的問題,
有二個作法,一個是在前端網路接收端加buffer,
一個是在後端要進AudioTrack時加buffer,
底下示範的方法就是第二個方法,

private static void queueAudioData(byte[] buf, int size) {  
 if((audioPcmBufferDataCount + size)> audioBufferSize)
 {
  return;  
 }
 if((audioPcmBufferFront + size) > audioBufferSize)
 {
  //rewind
  System.arraycopy(buf, 0, audioPCMData, audioPcmBufferFront, audioBufferSize - audioPcmBufferFront);
  audioPcmBufferDataCount += (audioBufferSize - audioPcmBufferFront);
  size -= (audioBufferSize - audioPcmBufferFront);
  audioPcmBufferFront = 0;      
 }
 System.arraycopy(buf, 0, audioPCMData, audioPcmBufferFront, size);
 audioPcmBufferFront += size;
 audioPcmBufferDataCount += size;
} 

audio decoder解出來的PCM data,
先透過queueAudioData()存到一個queue buffer,
之後再穩定的將資料write到AudioTrack
private class playAudio extends Thread {
 @Override
 public void run() {  
  super.run();
  int len = 512;
  while(!isInterrupted()) {
   try {
    if(audioPcmBufferDataCount < len)
    {
     Thread.sleep(10);
     continue;
    }

    if((audioPcmBufferEnd + len) > audioBufferSize)
     writeSize = audioBufferSize - audioPcmBufferEnd;
    else
     writeSize = len;
    playAudioTrack.write(audioPCMData, audioPcmBufferEnd, writeSize);
    Thread.sleep(1);
    audioPcmBufferDataCount -= writeSize;
    audioPcmBufferEnd += writeSize;
    if(audioPcmBufferEnd >= audioBufferSize)
     audioPcmBufferEnd=0;
    
    if(playAudioTrack.getPlayState()!=AudioTrack.PLAYSTATE_PLAYING) {     
          playAudioTrack.play();
       }
   } catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
   }       
  }
 }
}

[Android] MediaExtractor 和 MediaCodec 如何配合使用 - Audio篇

[Android] MediaExtractor 和 MediaCodec 如何配合使用 - Video篇裡,
我們示範了如何處理video,
現在我們來看audio部份,

原則上,我們可以使用同一個MediaExtractor來同時處理video/audio,
只要透過selectTrack()來動態切換即可,
不過,不知為何常常會收不到資料,
所以這邊我們就用固定的方式,
video和audio都有自己的MediaExtractor

audio處理方式和video大同小異,
除了audio不需要render到SurfaceView
以及audio需要多一個AudioTrack()來處理decode好的PCM data,

private MediaExtractor extractorAudio;
private MediaCodec decoderAudio;

extractorAudio = new MediaExtractor();
extractorAudio.setDataSource("myTest.mp4");

for (int i = 0; i < extractorAudio.getTrackCount(); i++) {
 MediaFormat format = extractorAudio.getTrackFormat(i);
 String mime = format.getString(MediaFormat.KEY_MIME); 
 if (mime.startsWith("audio/")) {  
  audioTrack = i;  
  extractorAudio.selectTrack(audioTrack);
  formatAudio = format;        
  decoderAudio = MediaCodec.createDecoderByType(mime);
  sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
  decoderAudio.configure(format, null, null, 0);
  break;
 }
}

if (audioTrack >=0) {
 if(decoderAudio == null)
 {
  Log.e(TAG, "Can't find audio info!");
  return;
 }
 else
 {
   // create our AudioTrack instance
   int minBufferSize = AudioTrack.getMinBufferSize(44100, AudioFormat.CHANNEL_CONFIGURATION_STEREO, AudioFormat.ENCODING_PCM_16BIT);
      int bufferSize = 4 * minBufferSize;
  playAudioTrack = new AudioTrack(
    AudioManager.STREAM_MUSIC,
    formatAudio.getInteger(MediaFormat.KEY_SAMPLE_RATE),
    AudioFormat.CHANNEL_OUT_STEREO,
    AudioFormat.ENCODING_PCM_16BIT,
    bufferSize,
    AudioTrack.MODE_STREAM
   );
  playAudioTrack.play();
  decoderAudio.start();
 }
}

extractorAudio一樣是根據MIME的資訊將audio track找出來,
decoderAudio.configure()我們只要傳進track format即可,其它參數都不需要,
另外,我們還需要多一個AudioTrack(),
因為decoderAudio最後解出來的是PCM data,
要實際發出聲音來,需要AudioTrack()幫忙,

下面是decode部份:
ByteBuffer[] inputBuffersAudio=null;
ByteBuffer[] outputBuffersAudio=null;
BufferInfo infoAudio=null;


if (audioTrack >=0)
{
 inputBuffersAudio = decoderAudio.getInputBuffers();
 outputBuffersAudio = decoderAudio.getOutputBuffers();
 infoAudio = new BufferInfo();
}
boolean isEOS = false;
long startMs = System.currentTimeMillis();
long lasAudioStartMs = System.currentTimeMillis();
while (!Thread.interrupted()) { 
 if (audioTrack >=0)
 { 
  if (!isEOS) {
   int inIndex=-1;
   try {
    inIndex = decoderAudio.dequeueInputBuffer(10000);
   } catch (Exception e) {
    e.printStackTrace();    
   }
   if (inIndex >= 0) {    
    ByteBuffer buffer = inputBuffersAudio[inIndex];
    int sampleSize = extractorAudio.readSampleData(buffer, 0);
    if (sampleSize < 0) {
     
     decoderAudio.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
     buffer.clear();
     isEOS = true;
    } else {     
     decoderAudio.queueInputBuffer(inIndex, 0, sampleSize, extractorAudio.getSampleTime(), 0);
     buffer.clear();
     extractorAudio.advance();
    }
    
   }
  }

  int outIndex=-1;
  try {
   outIndex = decoderAudio.dequeueOutputBuffer(infoAudio,10000);
  } catch (Exception e) {
   e.printStackTrace();   
  }

  switch (outIndex) {
  case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
   Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
   outputBuffersAudio = decoderAudio.getOutputBuffers();
   break;
  case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
   Log.d(TAG, "New format " + decoderAudio.getOutputFormat());
   playAudioTrack.setPlaybackRate(formatAudio.getInteger(MediaFormat.KEY_SAMPLE_RATE));
   break;
  case MediaCodec.INFO_TRY_AGAIN_LATER:
   Log.d(TAG, "dequeueOutputBuffer timed out!");
   break;
  default:
   if(outIndex>=0)
   {
    ByteBuffer buffer = outputBuffersAudio[outIndex];
    byte[] chunk = new byte[infoAudio.size];
    buffer.get(chunk);
    buffer.clear();
                if(chunk.length>0){         
                 playAudioTrack.write(chunk,0,chunk.length);
                }                
    decoderAudio.releaseOutputBuffer(outIndex, false);
   }
   break;
  }

  // All decoded frames have been rendered, we can stop playing now
  if ((infoAudio.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
   Log.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
   break;
  }
 }
}

if (audioTrack >=0)
{
 decoderAudio.stop();
 decoderAudio.release();
 playAudioTrack.stop();
}

extractorAudio.release();

和video差不多,就不解釋了,
唯一要注意的是decoderAudio.releaseOutputBuffer(outIndex, false)
後面的參數記得要設為false,也就是不需要render

[Android] MediaExtractor 和 MediaCodec 如何配合使用 - Video篇

MediaExtractor 是用來將來源的影音資料分開,
它支援HTTP streaming或者是local的檔案,
video和audio分開之後再丟給MediaCodec去decode,

首先看一下video部份,
video需要render,所以還另外需要加上SurfaceView
private MediaExtractor extractorVideo;
private MediaCodec decoderVideo;

extractorVideo = new MediaExtractor();
extractorVideo.setDataSource("myTest.mp4"); 

for (int i = 0; i < extractorVideo.getTrackCount(); i++) {
 MediaFormat format = extractorVideo.getTrackFormat(i);
 String mime = format.getString(MediaFormat.KEY_MIME);
 Log.d(TAG, "mime=>"+mime);
 if (mime.startsWith("video/")) {
  videoTrack = i;  
  extractorVideo.selectTrack(videoTrack);
  decoderVideo = MediaCodec.createDecoderByType(mime);
  decoderVideo.configure(format, surface, null, 0);
  break;
 }
}

if (videoTrack >=0) {
 if(decoderVideo == null)
 {
  Log.e(TAG, "Can't find video info!");
  return;
 }
 else
  decoderVideo.start();
}
extractorVideo 會根據MIME的資訊把video track找出來,
把video format和surface透過decoderVideo.configure()傳給decoderVideo,
之後decoderVideo便會接手decode和render的工作,

 接下來就是decode部份:
ByteBuffer[] inputBuffersVideo=null;
ByteBuffer[] outputBuffersVideo=null;
BufferInfo infoVideo=null;

if (videoTrack >=0)
{
 inputBuffersVideo = decoderVideo.getInputBuffers();
 outputBuffersVideo = decoderVideo.getOutputBuffers();
 infoVideo = new BufferInfo();
}

boolean isEOS = false;
long startMs = System.currentTimeMillis();

while (!Thread.interrupted()) {
 if (videoTrack >=0)
 {     
  if (!isEOS) {      
   int inIndex=-1;
   try {
    inIndex = decoderVideo.dequeueInputBuffer(10000);
   } catch (Exception e) {
    e.printStackTrace();    
   }

   if (inIndex >= 0) {
    ByteBuffer buffer = inputBuffersVideo[inIndex];
    int sampleSize = extractorVideo.readSampleData(buffer, 0);
    if (sampleSize < 0) {
     // We shouldn't stop the playback at this point, just pass the EOS
     // flag to decoder, we will get it again from the dequeueOutputBuffer     
     decoderVideo.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
     buffer.clear();
     isEOS = true;
    } else {     
     long current = System.currentTimeMillis();
     decoderVideo.queueInputBuffer(inIndex, 0, sampleSize, extractorVideo.getSampleTime(), 0);     
     buffer.clear();
     extractorVideo.advance();
    }
   }
  }
  int outIndex=-1;
  try {
   outIndex = decoderVideo.dequeueOutputBuffer(infoVideo,10000);
  } catch (Exception e) {
   e.printStackTrace();   
  }
  switch (outIndex) {
  case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
   Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
   outputBuffersVideo = decoderVideo.getOutputBuffers();
   break;
  case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
   Log.d(TAG, "New format " + decoderVideo.getOutputFormat());
   break;
  case MediaCodec.INFO_TRY_AGAIN_LATER:
   Log.d(TAG, "dequeueOutputBuffer timed out!");
   break;
  default:
   if(outIndex >=0)
   {
    ByteBuffer buffer = outputBuffersVideo[outIndex];    
    buffer.clear();
    decoderVideo.releaseOutputBuffer(outIndex, true);
    // We use a very simple clock to keep the video FPS, or the video
    // playback will be too fast
    while (infoVideo.presentationTimeUs / 1000 > (System.currentTimeMillis() - startMs)) {
     try {      
      sleep(10);
     } catch (InterruptedException e) {
      e.printStackTrace();
      Thread.currentThread().interrupt();
      break;
     }
    }
   }
   break;
  }

  // All decoded frames have been rendered, we can stop playing now
  if ((infoVideo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
   Log.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
   break;
  }
 }
}
if (videoTrack >=0)
{
 decoderVideo.stop();
 decoderVideo.release();
}

extractorVideo.release();

先呼叫decoderVideo.dequeueInputBuffer(10000)取得一個input buffer,
利用extractorVideo.readSampleData(buffer, 0)將資料放到buffer去,
然後再呼叫decoderVideo.queueInputBuffer()將資料queue到後面的decode工作, decoderVideo.dequeueOutputBuffer(infoVideo,10000)如果傳回非-1的值,
表示decode成功,
然後透過decoderVideo.releaseOutputBuffer(outIndex, true)將它render到surface,
這樣video的工作就完成了

2015年3月18日 星期三

[Android] 如何藉由JNI來呼叫底層Native的C/C++程式 - part I

基本上,Android提供的API已足夠一般寫程式的需求,
不過,有時候我們仍然會需要直接呼叫底層Native的C/C++程式,
有時是因為效能,有時是因為Android提供的功能不足...

如果你也是使用Eclipse,
那麼,你必須先安裝CDT和NDK,
之後,在你想要增加JNI介面的project按右鍵,
執行"Android Tools"/"Add Native Support"後,
你會發現project下面多了一個"jni"目錄,
目錄底下還有一個"x.cpp"和"Android.mk",
到這邊,你已經完全了第一步了...

接下來,Java要如何呼JNI的函式呢?

package myTest.com;
 
import android.app.Activity;
import android.os.Bundle;
 
public class MainActivity extends Activity { 
 private native String hello(String s); 
 
 static {
  System.loadLibrary("NativeMyTest");
 } 
}
"NativeMyTest"是你剛剛產生jni目錄時,輸入的名稱,
也就是說,待會會產生NativeMyTest.so檔,
 hello(String s)則是定義在JNI裡的函式,
 那JNI這邊呢?
#include <jni.h>
#include <stdio.h>

 #define LOG_TAG "MainActivity"
#define LOGI(...) __android_log_print(4, LOG_TAG, __VA_ARGS__);

JNIEXPORT void hello(JNIEnv* env, jobject obj, jstring str){ 
 LOGI("hello: %s", str);
}

jint JNI_OnLoad(JavaVM* pVm, void* reserved) {
  JNIEnv* env;
  if ((*pVm)->GetEnv(pVm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
  return -1;
  }
 
  JNINativeMethod nm[2];
  nm[0].name = "hello";
  nm[0].signature = "(Ljava/lang/String)V";
  nm[0].fnPtr = (void*)hello;

  jclass cls = (*env)->FindClass(env, "myTest/com/MainActivity");
  (*env)->RegisterNatives(env, cls, nm, 1);
  gJavaVM = pVm;
  return JNI_VERSION_1_6;
}
JNI_OnLoad()是用來載入那些會被Java程式所呼叫的函式,
FindClass()傳入的參數必須和Java程式的package和class名稱一樣,
執行時,看看是載入NativeMyTest.so有問題,還是沒有發現hello函式,
再來決定是那裡有問題...

2015年3月17日 星期二

[Android] Google語音辨識 - 關掉Beep聲

在前二篇裡,我們用了不同的方法來實現語音辨識,
[Android] Google語音辨識 - 使用RecognizerIntent
[Android] Google語音辨識 - 自己實作UI
不過,怎麼語音輸入一開始都會有一個Beep聲啊?
個人覺得那是個提示音,沒什麼不好,
但,如果你覺得吵,也可以把它關掉, 底下二種方法都可以:
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
am.setStreamSolo(AudioManager.STREAM_VOICE_CALL, true);
sr.startListening(recognizerIntent);
或者
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
am.setStreamMute(AudioManager.STREAM_SYSTEM, true);
sr.startListening(recognizerIntent);

[Android] Google語音辨識 - 自己實作UI

[Android] Google語音辨識 - 使用RecognizerIntent裡,
我們使用了RecognizerIntent來執行語音辨識,
但是,如果我們想要有自己特色的UI呢?

我們還另外需要SpeechRecognizer的幫忙
Intent recognizerIntent;
private SpeechRecognizer sr;

sr = SpeechRecognizer.createSpeechRecognizer(this);       
sr.setRecognitionListener(new listener());        
recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);        
recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
recognizerIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS,5);//5是你需要回傳多少個結果 

//開始辨識,將此code移到某個按鈕的onClick()裡
sr.startListening(recognizerIntent);  

//停止辨識,將此code移到某個按鈕的onClick()裡
//我們平常不需要做這個處理,語音辨識完畢它自己會停
//但如果我們想中斷它,或者有異常發生時,就可以呼叫底下的程式碼
sr.stopListening();
sr.cancel();
上面的listener(),代碼如下:
private static final int RECOGNIZER_ERROR_NETWORK_TIMEOUT = 1;//網路超時
private static final int RECOGNIZER_ERROR_NETWORK = 2;//其它的網路相關錯誤
private static final int RECOGNIZER_ERROR_AUDIO = 3;//無法錄音
private static final int RECOGNIZER_ERROR_SERVER = 4;//伺服器異常
private static final int RECOGNIZER_ERROR_CLIENT = 5;//其它手機端的異常
private static final int RECOGNIZER_ERROR_SPEECH_TIMEOUT = 6;//沒有語音輸入
private static final int RECOGNIZER_ERROR_NO_MATCH = 7;//沒有符合的辨識結果
private static final int RECOGNIZER_ERROR_RECOGNIZER_BUSY = 8;//RecognitionService 忙碌中
private static final int RECOGNIZER_ERROR_INSUFFICIENT_PERMISSIONS = 9;//權限不足

class listener implements RecognitionListener          
{
    public void onReadyForSpeech(Bundle params)
    { 
      //已經準備好可以接受語音輸入,可以在此時通知UI秀出相關icon,告知使用者開始說話             
    }
    public void onBeginningOfSpeech()
    {  
      //使用者開始說話,可以在此時通知UI秀出相關icon,告知使用者已收到聲音輸入了        
    }
    public void onRmsChanged(float rmsdB)
    { 
      //聲音輸入的強度變化,值是0~10
      //可以在此時通知UI秀出聲音大小的相關icon
      //onRmsChanged()來的非常頻繁和快速,最好是判斷有變化才通知UI更新
    }
    public void onBufferReceived(byte[] buffer)
    {       
    }
    public void onEndOfSpeech()
    {
       //使用者結束說話,系統會開始進行聲音辨識
    }
    public void onError(int error)
    {
       switch(error)
       {
        case RECOGNIZER_ERROR_NETWORK_TIMEOUT:         
         break;
        case RECOGNIZER_ERROR_NETWORK:         
         break;
        case RECOGNIZER_ERROR_AUDIO:         
         break;
        case RECOGNIZER_ERROR_SERVER:         
         break;
        case RECOGNIZER_ERROR_CLIENT:         
         break;
        case RECOGNIZER_ERROR_SPEECH_TIMEOUT:         
         break;
        case RECOGNIZER_ERROR_NO_MATCH:         
         break;
        case RECOGNIZER_ERROR_RECOGNIZER_BUSY:         
         break;
        case RECOGNIZER_ERROR_INSUFFICIENT_PERMISSIONS:         
         break;
       }
    }
    public void onResults(Bundle results)                   
    {
        //辨識的結果
        ArrayList data = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);         
        String firstMatched = (String) data.get(0);         
        txtResult.setText(firstMatched);
    }
    public void onPartialResults(Bundle partialResults)
    {
    }
    public void onEvent(int eventType, Bundle params)
    {
    }
}

[Android] Google語音辨識 - 使用RecognizerIntent

Google的語音辨識,
不管是中文或英文,
基本上準確度都還蠻高,
如果程式中需要語音辨識的功能,
方法如下:
private static final int RECOGNITION_REQUEST_CODE = 1234;//當成溝通的tag而且,數字隨便定

Intent it = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
it.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
//it.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
it.putExtra(RecognizerIntent.EXTRA_PROMPT, "請說...");
this.startActivityForResult(it, RECOGNITION_REQUEST_CODE);
上面的範例是直接呼叫RecognizerIntent,
所以會跳出Android已經定義好的UI,
它會自己處理收音,然後辨識,
 EXTRA_LANGUAGE_MODEL傳入的參數可以是RecognizerIntent.LANGUAGE_MODEL_FREE_FORM或RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH,
差別是什麼?
LANGUAGE_MODEL_FREE_FORM:通常我們會用這個參數,不管句子長短, LANGUAGE_MODEL_WEB_SEARCH:因為是網頁搜尋資料為base,通常是短句,對於中文的辨識程也較好,
不過,經測試結果,好像結果差異不大,呵...

 辨識好的結果呢?
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);    
    String firstMatched;  

    if(requestCode == RECOGNITION_REQUEST_CODE &amp;&amp; resultCode == RESULT_OK){
        //辨識結果不只一個,會從最接近的開始往後排序
        ArrayList<string> resultList = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
        firstMatched = (String)resultList.get(0);//通常我們只取第一個來用
    } else {
        firstMatched = "無法辨識";
    }
    
    result.setText(firstMatched);
}
整個辨識過程,要確定網路有連上線,
假如你想用離線的方式,
你可以到"設定"裡,有一個"下載離線語音辨識資料",
將想要的語言檔案下載回來,
如果網路沒開,還是可以進行語音辨識,
不過,實驗結果,辨識率就沒有那麼好了...

[Android] 為什麼我的thread停不了

通常thread的程式大概如下:
private class myThread extends Thread {
 @Override
 public void run() {
  super.run();
  Log.d(TAG,"myThread() entered...");
  while(!isInterrupted()) {
   Thread.sleep(1000);
    }
    catch (InterruptedException e) {   
     e.printStackTrace();    
    }
   }    
  }
  Log.d(TAG,"myThread() exited...");
 }
}
如果你想要停止這個thread,
我們可用interrupt()的方法,
例如,mMyThread = new myThread()是用來建立,
那mMyThread.interrupt()就是用來停止它,

可是我們發現Log.d(TAG,"myThread() entered...")會被執行,
表示mMyThread = new myThread()沒問題,
但是Log.d(TAG,"myThread() exited...")怎麼都沒被呼叫呢?
明明我們有呼叫mMyThread.interrupt()啊...

原來,thread可能在Thread.sleep()裡,
你呼叫interrupt()時,Thread會跑到catch (InterruptedException e),
但此時呢,interrupt會被reset,
所以回到while(!isInterrupted()),它還是成立,

那麼,我們該如何做呢,
private class myThread extends Thread {
 @Override
 public void run() {
  super.run();
  Log.d(TAG,"myThread() entered...");
  while(!isInterrupted()) {
   Thread.sleep(1000);
    }
    catch (InterruptedException e) {   
     e.printStackTrace(); 
     Thread.currentThread().interrupt();
    }
   }    
  }
  Log.d(TAG,"myThread() exited...");
 }
}
只要在"catch (InterruptedException e)"加上"Thread.currentThread().interrupt()"即可,
再設一次interrupt,
此時"Log.d(TAG,"myThread() exited...")"就會被執行到了

[Android] 如何複製檔案到system目錄

不管是"/system"或者"/data",預設都是read-only,
如果你在程式中想要將檔案複製過去,
需要先做一些處理,
程式範例如下:
Process process = Runtime.getRuntime().exec("su");
DataOutputStream output = new DataOutputStream(process.getOutputStream());
output.writeBytes("mount -o remount,rw -t yaffs2 /dev/block/mtdblock3 /system\n");
String sCatCommand = "cat "+Environment.getExternalStorageDirectory()+"/myTest/test.dat"+ "> /system/"+"test.dat"+"\n";
output.writeBytes(sCatCommand);
output.writeBytes("mount -o remount,ro -t yaffs2 /dev/block/mtdblock3 /system\n");
output.writeBytes("exit\n");  
output.flush();
process.waitFor();
上面的範例,是將SD卡裡的"/myTest/test.dat"拷貝到"/system"

[Android] 關於服務Service - 如何讓Service永遠不被Killed

如前一篇所說([Android] 關於服務Service - Service會被Killed?),
雖然Service被Killed後,通常系統會再重啓它,
但...萬一系統沒有這麼做呢?
我們是否有另外的方法可以保護自己的Service永生不滅?

有一個簡單方式,就是再建一個monitorService,
它的工作就是發現myService不見時,負責把myService再重啓,

如同[Android] 如何在開機後自動載入程式裡的做法,
有一個BootUpReceiverClass.java來接收開機時系統的訊息,
但其實它同時也可以接收Broadcast過來的訊息,
我們需要在AndroidManifest.xml增加一個intent-filter,
<receiver android:enabled="true" android:name="myTest.com.BootUpReceiverClass"
 android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
 <intent-filter>
 <action android:name="android.intent.action.BOOT_COMPLETED" />
 <category android:name="android.intent.category.DEFAULT" />
 </intent-filter>
 <intent-filter>
 <action android:name="myTest.com.BootUpReceiverClass" /> 
 <category android:name="android.intent.category.DEFAULT" />       
 </intent-filter>
</receiver> 
接下來,就是monitorService部份,
假設我們在monitorService裡建了一個monitorThread,
它負責檢查"myTest.com.myServiceClass"有沒有在執行,
如果沒有,就sendBroadcast()給"myTest.com.BootUpReceiverClass"
private boolean isMyServiceRunning(String className) {
  try
  {
      ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
      for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {       
          if (className.equals(service.service.getClassName())) {
              return true;
          }
      }
  }catch (Exception e) {   
   e.printStackTrace();   
  }
     return false;
 }
 private class monitorThread extends Thread {
  @Override
  public void run() {
   super.run();   
   while(!isInterrupted()) {
    try {     
     if(!isMyServiceRunning("myTest.com.myServiceClass"))
     {      
      try
      {
       Intent intent = new Intent("myTest.com.BootUpReceiverClass");
       if(intent!=null)
       {        
           sendBroadcast(intent);
                 Thread.sleep(15000);
       }
      }catch (Exception e) {       
       e.printStackTrace();       
      }
     } 
     else
     {      
      Thread.sleep(3000);
     }     
    } catch (InterruptedException e) {     
     e.printStackTrace();     
    }
   }   
  }
 }
那..如果monitorService也被kill掉了呢?
所以,在myService裡也要一個類似的機制,
一直監視monitorService有沒有在執行,
如果不見了,就把它重啓,
程式碼是類似的,這邊就不貼了,

至於多久去檢查對方在不在呢?
經驗上,2~5秒應該都可以,
你可以去應用程式的執行中列表去看myService和monitorService是不是都在,
並且,試著關掉myService,看看會不會被重啓,
反過來,關掉monitorService,看看會不會被重啓...

那...如果monitorService和myService同時被killed呢?
人工去砍是很難成功的,
因為其中一個被關掉,立刻就會另一個重啓,
除非是系統才有辦法一次關二個,
這也表示,系統loading或記憶體真的無法負荷了,
就算myService仍然活著,也會卡卡的吧...

[Android] 關於服務Service - Service會被Killed?

是的,Service會被killed,尤其是Low Memory時,
如果系統loading不重時呢?還是有可能,
以下就Service相關的一些設定來討論一下...

1. onStartCommand()的回傳值
START_STICKY:之前的範例裡,我們就是採用這回傳值,
Service被killed後,在系統loading允許下,Service會被重新啓動,
不過,傳進來的intent可能是null,
如果我們需要從intent取得資料,要先做判斷:
@Override
 public int onStartCommand(Intent intent, int flags, int startId) {
  if(intent != null)
  {
   String sData1=Intent.getStringExtra("DATA_KEY1");
   int iData2=Intent.getIntExtra("DATA_KEY2",0);
   mServiceThread = new ServiceThread();
   mServiceThread.start();
   }
   else
   {
     Intent i = new Intent(xxx);
     sendBroadcast(i);
   }
    return  START_STICKY ; 
 }      
如果你就是需要這些資料才能開始Service,
那麼,你需要透過sendBroadcast(),
發一個broadcast event給當初啓動Service的Activity,
從那裡再啓動Service一次,順便把資料帶入,
經實驗結果,Service大概在30分鐘左右會被重啓(每個裝置會不同),
尤其是Service在idle時,
例如監控網路通訊,如果都沒有封包,
Service一直沒事做,那它可能就會被killed,然後很快又被重啓,
你可以看到執行中,Service不見了幾秒,
目前是沒遇到重啓失敗的情形
START_NOT_STICKY: Service被killed就結束了,不會自動重啓
START_REDELIVER_INTENT: Service被killed後會被重啓,
並且傳入最後的intent,
也就是說,intent不會是null,
不過,不知為何,我使用這個參數時程式會hang up

2. 修改AndroidManifest.xml
有人建議可以加上<intent-filter android:priority="1000"></intent-filter>來提高Service的Priority,
經實際結果,被重啓時間有稍微拉長些,大概60分鐘會重啓一次

其它還有人試著在onDestroy()裡重啓自己,
但因為Service被killed時,onDestroy()不見得會被呼叫,
所以此法我就沒有試了...

不管怎樣,怎麼好像都會被重啓呢?
有沒有不被重啓的方法?
可以試試把Service設成Foreground Service:
Notification notification = new Notification(R.drawable.launch, "Your Application Name", System.currentTimeMillis());  
Intent notificationIntent = new Intent(this, YourMainActivity.class);  
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);  
notification.setLatestEventInfo(this, "Your Application Name", "Your Service Name", pendingIntent);  
startForeground(1, notification);
使用Foreground方式,啓動後,你會看到在狀態列有一個你自定的icon, 經實際結果,跑了一天,Service從未被重啓過

2015年3月16日 星期一

[Android] 關於服務Service - 如何傳遞資料給Service

有時,我們需要傳遞一些資料給Service,
例如,播放檔案的檔名,或者控制訊息等等,
有幾個方法可以使用,
第一個就是利用Intent之間傳遞資料的方法,
在開始Service時,將資料帶入,
具體做法如下:
Intent i = new Intent(this,myServiceClass.class);
i.putExtra("DATA_KEY1","my data");
i.putExtra("DATA_KEY2",1234);
startService(i);
在這裡我們放入"my data"這個字串和1234的數值,

Service端要如何取出資料呢?
@Override
 public int onStartCommand(Intent intent, int flags, int startId) {
  String sData1=Intent.getStringExtra("DATA_KEY1");
  int iData2=Intent.getIntExtra("DATA_KEY2",0);
  mServiceThread = new ServiceThread();
  mServiceThread.start();
     return  START_STICKY ; 
 }   
onStartCommand()裡,我們可以取出傳過來的資料,
也就是說,sData1就是"my data"iData2就是1234, 
你可以一直重覆呼叫startService(),傳入你需要的資料,
如果重覆呼叫startService(),onStartCommand()就會對應的被呼叫,
當然,在上面的例子,onStartCommand()裡面會執行new ServiceThread()mServiceThread.start(), 這部份需要另外處理,避免也重覆到了...

另外還有一個常用的方式,就是利用bindService的機制,
public class myService  extends Service {
 private ServiceThread mServiceThread; 
 @Override
 public IBinder onBind(Intent intent) {
  // TODO Auto-generated method stub
  return new myServiceLocalBinder();
 }
 
 @Override
 public void onCreate() {
 // TODO Auto-generated method stub
 super.onCreate();
 }
  
 @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
  mServiceThread = new ServiceThread();
  mServiceThread.start();
     return  START_STICKY ; 
 }     
  
 public class myServiceLocalBinder extends Binder {  
  myService getService() {  
            // Return this instance of LocalService so clients can call public methods  
            return myService.this;  
        }     
    }

 public void SendData2Service(String s, int i)
 {
   //your action here
 }
  
  
 @Override
 public void onDestroy() {
  Log.d(TAG,"onDestroy()");
  if (mServiceThread != null)
   mServiceThread.interrupt();
 }
  
 private class ServiceThread extends Thread {
  @Override
  public void run() {
   super.run();
   //your action here
  }
 } 
}
在啓動Service的地方,修改如下:
myServiceClass myService=null; 

private ServiceConnection sMyServiceConnection = new ServiceConnection() {    
 @Override  
 public void onServiceDisconnected(ComponentName name) {  
 }    
 
 @Override
 public void onServiceConnected(ComponentName name, IBinder service) {
  myService = ((myServiceClass.myServiceLocalBinder) service).getService();  
 }  
};  

 public void StartMyService() {
   Intent i = new Intent(this, myService.class);  
   bindService(i, sMyServiceConnection , BIND_AUTO_CREATE);
   startService(i);
 }
 
 public void StopMyService(){
  Intent i = new Intent(this,myServiceClass.class);
 try{
     unbindService(sMyServiceConnection);
     stopService(i);
  } catch (IllegalArgumentException e){              
 } 
 }
當你想要傳送資料給Service,可以利用myService.SendData2Service()

[Android] 關於服務 Service - 如何建立

平常我們都是在Activity裡建構我們的程式,
包括和使用者的互動以及想要完成的事,
那麼,為何還需要所謂的服務(Service)呢?
想像一下,你可能需要在背景播放音樂,
或者執行TCP/UDP等網路活動,
這些你當然也可以new thread背後去跑,
但...假如我們希望在Activity結束後,
這些服務仍然存在呢?
此時你就需要Service的機制...

建立一個Service只要幾個步驟:
首先,先create service class..
public class myService  extends Service {
 private ServiceThread mServiceThread; 
 @Override
 public IBinder onBind(Intent intent) {
  // TODO Auto-generated method stub
  return null;
 }
 
 @Override
 public void onCreate() {
 // TODO Auto-generated method stub
 super.onCreate();
 }
  
 @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
  mServiceThread = new ServiceThread();
  mServiceThread.start();
     return  START_STICKY ; 
 }     
  

 @Override
 public void onDestroy() {
  Log.d(TAG,"onDestroy()");
  if (mServiceThread != null)
   mServiceThread.interrupt();
 }
  
 private class ServiceThread extends Thread {
  @Override
  public void run() {
   super.run();
   //your action here
  }
 } 
}
這裡new ServiceThread()是因為一般Service會需要一直執行某些動作,
如果你只需要簡單的處理一些事情,而且只要一次,
那把你要做的事寫在在onStartCommand()即可... 
再來,Service必須在AndroidManifest.xml裡註冊才行
<service android:name="myPackageName.myService"> </service >
如此就已建立Service了...
那如何調用呢?
假設你的Activity裡有一個按鈕,
按下按鈕後我們希望啓動剛剛的Service
public void onClick(View v) {
Intent i = new Intent(this, myService.class);  
startService(i);
}
反過來,如果你想要停止這個Service呢?
public void onClick(View v) {
Intent i = new Intent(this, myService.class);  
stopService(i);
}
如果要確定Service有沒有建立成功, 可以到應用程式管理員的"執行中"畫面找一下,看看有沒有剛剛建立的Service名字...

2015年3月11日 星期三

[Android] 如何利用Assets目錄夾帶檔案

很多時候,我們會在安裝完apk檔後,需要存取自己的檔案,
例如,我們可能想要播放自己預設的mp3檔,
有一個簡單的作法就是在開發時,將想要的檔案放在assets目錄裡,
這個目錄裡的資料,在你開發時,它不會發生任何事,
但當你包裝apk時,裡面的資料會同時包進去,
那麼,安裝完apk後,如何存取它呢?
有些人可能會用"file://android_asset/"來存取它,
不過,建議還是用AssetManager, 使用方式也很簡單,
假設你本來在assets目錄放了一個t.mp3,
AssetManager assetManager = getAssets();      
InputStream in = null;
OutputStream out = null;
File sdCardDir = Environment.getExternalStorageDirectory();    
in = assetManager.open("t.mp3");
out = new FileOutputStream(new File(sdCardDir+"/myFolder", "t.mp3"));    
byte[] buffer = new byte[1024];
int read;
while((read = in.read(buffer)) != -1)
{
  out.write(buffer, 0, read);
}    
in.close();
in = null;
out.flush();
out.close();
out = null;

上面的方式是將assets裡的檔案copy到"/myFolder",
然後我們就可以用FileInputStream等檔案存取的方法來存取它,

當然,還有不同的方式,例如,你想要直接播放它的話:
AssetFileDescriptor fileDescriptor = getAssets().openFd("t.mp3");   

MediaPlayer mp = new MediaPlayer();

mp.setDataSource(fileDescriptor.getFileDescriptor());

[Android] 如何在開機後自動載入程式

要能在開機後自動載入自己的程式,需要修改幾個地方,
假設MainActivity是程式的開始Activity,
首先,需要先增加一個BootUpReceiverClass.java來接收開機時系統的訊息
 public class BootUpReceiverClass extends BroadcastReceiver {
     @Override
     public void onReceive(Context context, Intent intent) {      
         Intent i = new Intent(context, MainActivity.class);  
            i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(i);          
         }
    }

再來,AndroidManifest.xml需要增加權限
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
另外,在<application> </application>裡增加receiver的tag

   
       
       
      
    

[Android] MediaPlayer RTSP 串流

MediaPlayer如果要播放串流,只要在setDataSource()裡設定串流路徑即可,
但,如果是需要帳號和密碼呢?
加密有二種格式:
1. Basic Authentication

Context context = getApplicationContext();  
sUrl = "rtsp://xxxx";  
sAccount = "xx";  
sPassword = "xx";  
Uri source = Uri.parse(sUrl);  
Map<String, String> headers = getRtspHeaders();  
mediaPlayer.setDataSource(context, source, headers);

Map<String, String> getRtspHeaders() {  
  Map<String, String> headers = new HashMap<String, String>();  
   String basicAuthValue = getBasicAuthValue(sAccount, sPassword);  
   headers.put("Authorization", basicAuthValue);  
   return headers;  
}  
private String getBasicAuthValue(String usr, String pwd) {  
   String credentials = usr + ":" + pwd;  
   int flags = Base64.URL_SAFE | Base64.NO_WRAP;  
   byte[] bytes = credentials.getBytes();  
   return "Basic " + Base64.encodeToString(bytes, flags);       
}    
上面是處理basic authentication的方式,
另外一個簡單的方式,是在路徑裡直接給定帳號和密號,例如: sUrl = "rtsp://admin:pwd@xxxx";

2. Digest Authentication
原本以為,如果是digest authentication,用sUrl = "rtsp://admin:pwd@xxxx"可能也可以, 
但總是得到"error 100, Media server died"之類的錯誤訊息,
  查了一下網路,得到的大都是 MediaPlayer不支援rtsp digest authentication的答案, 
原本想放棄了,某天心血來潮,想說用Wireshark來抓一下封包看看,
結果發現,
在RTSP DESCRIBE封包裡,它的格式都對啊,也支援digest authentication,
那為何還是得到"401 unauthorized"的Reply呢?
 (以上的截圖,有可能不是原來用MediaPlayer時所抓的,日子久了,有點忘記,但以下的分析的確是MediaPlayer的)
研究了一下DESCRIBE封包,username沒錯,uri沒錯,nonce是前一次發DESCRIBE時,RTSP server reply回來的,比了一下,也沒錯,
難道,是response算錯了?
response= md5(md5(username:realm:password):nonce:md5(uri));
自己手動算了一下,耶?真的有問題...
自己算的response和DESCEIBE 封包裡的response不一樣,
沒辦法,只好去看一下android裡的有關authentication 的 source codes,
原來在ARTSPConnection.cpp裡,它在算response時,
所用的realm不是從RTSP server Reply時取出的,
而是hard code成"Streaming Server",
難怪算出來的response不對,
雖然發現原因了,但好像也不能幹嘛,
因為我們只能透過 setDataSource(), 之後的事,有問題,也無能為力...
看來,要用RTSP Digest Authentication,只能放棄 MediaPlayer,改採它法

2015年3月10日 星期二

[Android] MediaPlayer 基本使用方法

要播放音樂或影片,最簡單的方法就是使用 MediaPlayer
通常我們可以用以下方法:
private MediaPlayer mp = new MediaPlayer();
mp.setDataSource("/sdcard/test.mp3");//Set the source
mp.prepare();
mp.start();
如果是網路上的串流,一樣可以,例如: mp.setDataSource("rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov");
基本上,http或rtsp都可以, 
如果是拿 MediaPlayer 來播於影片,那就還需要SurfaceView的配合
此時,new MediaPlayer()就必需在surfaceCreated()裡宣告,類似以下方式:
@Overridepublic void surfaceCreated(SurfaceHolder sh) {
         mp= new MediaPlayer();
         mp.setDisplay(surfaceHolder);   
         mp.setOnPreparedListener(this);      
    }
@Override
     public void surfaceDestroyed(SurfaceHolder arg0) {
         mp.release();
     }

@Override
public void onPrepared(MediaPlayer mp) {
        mp.start();
     }
如果有一個按鈕來控制播放,可以在按鈕的onClick()裡,
mp.setDataSource("your source path");
mp.prepareAsync();  
MediaPlayer可以支援的檔案格式還蠻多的,MP3,MPEG2,H.264,3gp....