前回はタイムシフトバッファリング用に4.1.1_r1のDiskLruCache.javaを改造してみました。
名前は載せてなかったけど、TimeShiftDiskCacheにしました。そのまんまやん(*´ڡ`●)
今回はTimeShiftDiskCacheを使って常時タイムシフトバッファリングするためのサービスを作ってみます。
なんでサービスにせなあかんかはその1を見てな。
では早速と言うところなんやけど、その前に少し寄り道をします。
プログラムに限らず人にはそれぞれポリシーがあります。企業勤めな人であればそれぞれの企業のローカルルールもあります。
自分の場合は、後で流用できそうなもの・共通で繰り返し使えそうなものは別クラスに入れるようにしています。
まぁこれは人それぞれ好き好きですけど。
という事で今回作るサービスもSDKのServiceクラスを直接使う代わりに間に1つ別のクラスを挟んでいます。
ドカン^^
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
public abstract class BaseService extends Service { private static final boolean DEBUG = false; // FIXME set false on production private static final String TAG = BaseService.class.getSimpleName(); protected final Object mSync = new Object(); private final Handler mUIHandler = new Handler(Looper.getMainLooper()); private Handler mAsyncHandler; private LocalBroadcastManager mLocalBroadcastManager; private volatile boolean mDestroyed; @Override public void onCreate() { super.onCreate(); if (DEBUG) Log.v(TAG, "onCreate:"); final Context app_context = getApplicationContext(); synchronized (mSync) { mLocalBroadcastManager = LocalBroadcastManager.getInstance(getApplicationContext()); final IntentFilter filter = createIntentFilter(); if ((filter != null) && filter.countActions() > 0) { mLocalBroadcastManager.registerReceiver(mLocalBroadcastReceiver, filter); } if (mAsyncHandler == null) { mAsyncHandler = HandlerThreadHandler.createHandler(getClass().getSimpleName()); } } } @Override public void onDestroy() { if (DEBUG) Log.v(TAG, "onDestroy:"); mDestroyed = true; synchronized (mSync) { if (mAsyncHandler != null) { try { mAsyncHandler.getLooper().quit(); } catch (final Exception e) { // ignore } mAsyncHandler = null; } if (mLocalBroadcastManager != null) { try { mLocalBroadcastManager.unregisterReceiver(mLocalBroadcastReceiver); } catch (final Exception e) { // ignore } mLocalBroadcastManager = null; } } super.onDestroy(); } protected boolean isDestroyed() { return mDestroyed; } /** * create IntentFilter to receive local broadcast * @return null if you don't want to receive local broadcast */ protected abstract IntentFilter createIntentFilter(); /** BroadcastReceiver to receive local broadcast */ private final BroadcastReceiver mLocalBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { if (DEBUG) Log.v(TAG, "onReceive:" + intent); try { onReceiveLocalBroadcast(context, intent); } catch (final Exception e) { Log.w(TAG, e); } } }; protected abstract void onReceiveLocalBroadcast(final Context context, final Intent intent); /** * local broadcast asynchronously * @param intent */ protected void sendLocalBroadcast(final Intent intent) { synchronized (mSync) { if (mLocalBroadcastManager != null) { mLocalBroadcastManager.sendBroadcast(intent); } } } //================================================================================ //================================================================================ protected Handler getAsyncHandler() { if (mDestroyed) throw new IllegalStateException("already destroyed"); synchronized (mSync) { return mAsyncHandler; } } protected void runOnUiThread(final Runnable task) { if (task == null) return; if (mDestroyed) throw new IllegalStateException("already destroyed"); mUIHandler.removeCallbacks(task); mUIHandler.post(task); } protected void runOnUiThread(final Runnable task, final long delay) { if (task == null) return; if (mDestroyed) throw new IllegalStateException("already destroyed"); mUIHandler.removeCallbacks(task); if (delay > 0) { mUIHandler.postDelayed(task, delay); } else { mUIHandler.post(task); } } protected void removeFromUiThread(final Runnable task) { mUIHandler.removeCallbacks(task); } protected void queueEvent(final Runnable task) throws IllegalStateException { if (task == null) return; if (mDestroyed) throw new IllegalStateException("already destroyed"); queueEvent(task, 0); } protected void queueEvent(final Runnable task, final long delay) throws IllegalStateException { if (task == null) return; if (mDestroyed) throw new IllegalStateException("already destroyed"); synchronized (mSync) { if (mAsyncHandler != null) { mAsyncHandler.removeCallbacks(task); if (delay > 0) { mAsyncHandler.postDelayed(task, delay); } else { mAsyncHandler.post(task); } } else { throw new IllegalStateException("worker thread is not ready"); } } } protected void removeEvent(final Runnable task) { synchronized (mSync) { if (mAsyncHandler != null) { mAsyncHandler.removeCallbacks(task); } } } } |
昔作ったプログラムから使いそうな部分を抜粋してきました。まぁこれぐらいのボリュームで1つしか継承しないのであればクラスを分けずに直接アプリのロジックが入っているクラスに入れてしまってもいいですけど。
本当のところを言えばJavaもC++の様に多重継承出来れば、Activity・Fragment・Service…色んなクラスで共通の処理のクラスを1つ作れば済むんですけど。
閑話休題
TimeShiftRecServiceを実装する
TimeShiftRecServiceの実装・実行形態を決める
今回サービス内で実行する処理は、本来はMediaCodecのエンコーダーとMediaMuxerだけで出来るはずのことです。それをわざわざサービスにしている理由は、単にエンコード・ファイルへの書き出し処理に時間がかかるために通常のアプリ/Activity/Fragmentのライフサイクル内では全ての処理を終了出来ない、という所にあります。一方、映像データそのものは通常のアプリ側から供給してもらうことになります。
別な言い方をすればActivity/Fragmentとは異なるライフサイクルを持たせるためだけにサービス化しています。
御存知の通りAndroidにおけるServiceにはいくつか実装・実行方法があり、それによってServiceのライフサイクルも異なります。
1つはstartService/stopServiceを使う方法ですが、一旦サービスを起動するとActivity/Fragmentとから簡単にサービスを制御することはできませんし、今回のように同じサービスへ継続して映像データを送り込むというのも簡単にはできません。
でもできるだけMediaCodecのエンコーダー+MediaMuxerを使うのと近い感覚で使えたほうが便利ですよね。とすると映像データをActivity/Fragmentから供給する&任意のタイミングでファイルの書き出しを開始/停止する必要があります。
という事で今回のServiceはbindService/unbindServiceを使ってServiceとのコネクションを確立するという形、それも別プロセスで起動する必要もAIDLを介してアクセスする必要もなく、一番簡単なローカルサービスとして実装・利用したいと思います。
忘れちゃならん事
Androidでサービスを作っていざ動かそうとしても原因不明でまったく動かん? そんな経験ありませんか?
その原因の1つはAndroidManifest.xmlにサービスの記述を行っていないことかもしれません。
ということで、兎にも角にもAndroidManifest.xmlにサービスの記述をすること!
今回はタイムシフト関係の実装をtimeshiftモジュールに入れましたので、timeshiftモジュールのAndroidManifest.xmlにサービスの記述を行います。こうすればtimeshiftモジュールを使う他のモジュールでは特に何もせずに自動的にAndroidManifest.xmlがマージされサービスの記述が追加されます。例えばこんな感じになります。
1 2 3 4 5 6 7 8 9 10 11 12 |
<manifest package="com.serenegiant.timeshift"> <application xmlns:android="http://schemas.android.com/apk/res/android"> <service android:name="com.serenegiant.timeshift.TimeShiftRecService" android:exported="false"> <intent -filter> <action android:name="com.serenegiant.timeshift.TimeShiftRecService"></action> </intent> </service> </application> </manifest> |
serviceに限らずActivityでもuses-featureでもpermissionでもそのモジュールを使う時に必要となるAndroidManifest.xmlの項目はなんでも書いておけます…が必要最小限にしましょうね。どこぞのライブラリのように使ってもいないREAD_PHONE_STATEを問答無用でマージさせるのはやめましょう。マージしたAndroidライブラリが勝手に付与するパーミッションを取り除く
TimeShiftRecServiceの取りうる状態を決める
もっと細かくももっと粗くもできるけど、今回は次の7つの状態を取ることにしました。まぁSTATE_UNINITIALIZEDとかは要らんなぁとは思うけど^^;
- STATE_UNINITIALIZED
- STATE_INITIALIZED
- STATE_PREPARING
- STATE_READY
- STATE_BUFFERING
- STATE_RECORDING
- STATE_RELEASING
ローカルサービスとしても体裁を整える
さっきのAndroidManifest.xmlの記述もそうやけど、ローカルサービスとして実装する上での最小限要る部分を書いてしまいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public class TimeShiftRecService extends BaseService { /**Binder class to access this local service */ public class LocalBinder extends Binder { public TimeShiftRecService getService() { return TimeShiftRecService.this; } } private final Object mSync = new Object(); /** binder instance to access this local service */ private final IBinder mBinder = new LocalBinder(); @Override public void onCreate() { super.onCreate(); } @Override public void onDestroy() { super.onDestroy(); } @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { if (DEBUG) Log.i(TAG, "onStartCommand:startId=" + startId + ": " + intent); super.onStartCommand(intent, flags, startId); return START_STICKY; } @Nullable @Override public IBinder onBind(final Intent intent) { return mBinder; } @Override public boolean onUnbind(final Intent intent) { return false; // onRebind使用不可 } } |
たぶんこれだけでServiceとしてビルドして実行できるようになります。多分。
フォアグラウンドサービス
もう1つ大事なことがあります。Android6以前だとこのままでも大抵大丈夫なのですが、Android6以降では従来よりもアグレッシブにアプリを停止させようとします。サービスも例外ではなく、特にbindのみを提供するアクティブなActivityと関係していないサービスは停止させられてしまいます。
でも、Activityのライフサイクルに合わせてサービスを止められてしまうとそもそもの今回の目的が達成されなくなってしまうおそれがあります。
Android Developersには次のような記述があります。
コンポーネントが startService() を呼び出してサービスを開始すると(結果的に onStartCommand() が呼び出される)、stopSelf() を使ってサービス自身が停止するか、他のコンポーネントが stopService() を呼び出して停止するまで、サービスは実行し続けます。
コンポーネントが bindService() を呼び出してサービスを作成した(そして onStartCommand() が呼び出されていない)場合、サービスはコンポーネントにバインドされている間のみ実行します。 すべてのクライアントからアンバインドされると、サービスはシステムによって破棄されます。
Android システムは メモリが少なくなって、ユーザーが使用しているアクティビティ用のシステムリソースを回復させる必要が生じた場合のみ、サービスを強制的に停止させます。 サービスがユーザーが使用しているアクティビティにバインドされている場合は、 それが強制終了される可能性は低く、フォアグラウンドで実行(後で説明)するように宣言されている場合は、強制終了されることはほとんどありません。 一方で、サービスが開始されてから長時間実行している場合は、システムはバックグラウンド タスクのリストにおけるその位置付けを徐々に低くし、そのサービスが強制終了される確率が高くなります。 開始されたサービスを作成する際は、システムによる再起動を円滑に処理するようデザインする必要があります。 システムがサービスを強制終了すると、リソースが回復次第そのサービスが再起動します(後述の onStartCommand() から返される値にもよります)。 システムがサービスを破棄するタイミングについては、プロセスとスレッドのドキュメントをご覧ください。
まぁこれが公式見解っちゅうことだと思いますが、実際には即行で止めてくる端末もあればunbindされてからもずっと動き続けていられる端末もあります。
でも端末によって挙動が変わるというのは困るので、次のようにします。
- startServiceも併用してサービスを起動します
ただし、サービスが必要なくなった時に自力でstoSelfを呼び出すようにする必要があります。 - フォアグラウンドサービスとして実行されるようにします
ただし、サービスが必要となくなった時にフォアグラウンドサービスとしての登録を解除する必要があります。
先程のコードで#onStartCommandが実装されてあったのはそういうわけだったんですよね。こうするとまぁよっぽどメモリが足りんとかにならん限りは好きなだけ動いていられます。
例えばメインActivityの#onCreateで
1 2 |
final Intent serviceIntent = new Intent(this, TimeShiftRecService.class); startService(serviceIntent); |
のように呼び出しておきます。
(その代わり不要になった時にはちゃんとサービス内でstopSelfを呼ばにゃならんからな)
もう1つのフォアグラウンドサービスとしても実行…これも今はAndroid Developersに記述があります。なので省略(^o^)vってなわけにはいかんやろうから概略だけ。
サービスをフォアグラウンドサービスとして実行するには、次の2つの事を実行する必要があります。
- ステータスバーに通知を表示する
- startForegroundを呼び出す
またフォアグラウンドから除去するには次の2つを実行する必要があります。
- ステータスバーから通知を除去する
- stopForegroundを呼び出す
上の内容をみて鋭い人は実装するものが増えた事に気づいたはず。さぁていったいなんでしょうか?
おややぁ、思っていたところまでたどりつけんかったけど、長くなってきたので今日はここまで。
今日はあんまりタイムシフト録画自体とはあんまり関係なことばっかりやった(汗)
お疲れ様でした。
(^.^)/~~~