久々の長編や^^ 大した事ないのを長々と書いとるとも言う^^;
…実は、タイムシフト録画サービスは1つ1つのコード・テクニックは初心者レベルなのばっかりやから、あんまり何書いてええか良う分からへんねん。
前回はフォアグラウンドサービスにせにゃならんという話でおしまいでした。
最後に「上の内容をみて鋭い人は実装するものが増えた事に気づいたはず。さぁていったいなんでしょうか?」なんてことを書きましたが分かったでしょうか?
色々ありますが、フォアグラウンドサービスと直接関係するもの、それはステータスバーに通知を出すことです^^
という事でまずはステータスバーに通知を出してフォアグラウンドサービスにしてやるぜぇー(^o^)/
ステータスバーに通知を出してフォアグラウンドサービスにする
ステータスバーに通知を出すにはNotificationManagerちゅうのを使いまチュウ。
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 |
private static final int NOTIFICATION = R.string.time_shift; private final Object mSync = new Object(); private NotificationManager mNotificationManager; @Override public void onCreate() { super.onCreate(); if (DEBUG) Log.v(TAG, "onCreate:"); synchronized (mSync) { mNotificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); } } @Override public void onDestroy() { if (DEBUG) Log.v(TAG, "onDestroy:"); synchronized (mSync) { mState = STATE_UNINITIALIZED; mIsBind = false; stopForeground(true/*removeNotification*/); if (mNotificationManager != null) { mNotificationManager.cancel(NOTIFICATION); mNotificationManager = null; } } super.onDestroy(); } protected void showNotification(final CharSequence text) { final Notification notification = new Notification.Builder(this) .setSmallIcon(R.mipmap.ic_timeshift) // the status icon .setTicker(text) // the status text .setWhen(System.currentTimeMillis()) // the time stamp .setContentTitle(getText(R.string.time_shift)) // the label of the entry .setContentText(text) // the contents of the entry .setContentIntent(createPendingIntent()) // The intent to send when the entry is clicked .build(); // Send the notification. synchronized (mSync) { if (mNotificationManager != null) { startForeground(NOTIFICATION, notification); mNotificationManager.notify(NOTIFICATION, notification); } } } /** * サービスのティフィケーションを選択した時に実行されるPendingIntentの生成 * 普通はMainActivityを起動させる * @return */ protected PendingIntent createPendingIntent() { try { // 普通はこんな感じ // return PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0); // でも今回は別モジュールにして、サービスにバインドする時にメインActivityのクラスを渡すようにしてみた return PendingIntent.getActivity(this, 0, new Intent(this, mActivityClass), 0); } catch (final Exception e) { return null; } } |
今回はタイムシフト用のサービスを後で使いまわしやすいようにモジュールに入れて継承せずにそのまま使えるようにしたかったので、バインドする時にメインActivityのクラスをIntentに入れて渡すようにしています。
継承しても良ければabstractのサービスを作って置いて先程のcreatePendingIntentメソッドをabstractにして、使用するアプリ内でその抽象サービスを継承したクラスを作ってそこでcreatePendingIntentメソッドを実装してあげればよかです。
でも継承するようにすると、モジュールのAndroidManifest.xmlでサービスを追加できんくなるんですよねぇ。
普段は自分も継承して使うタイプの作り方をしますが、今回は
- モジュールをbuild.gradleに追加するだけで完結して使えるようにしたい
- つまりモジュールのAndroidManifest.xmlにサービスを記述せんとあかん
- abstractなサービスはAndroidManifest.xmlに追加でけん
- 実はjava.lang.ClassはSerializableやからIntentで送れるねん
- ローカルサービスやねん
- こんなこともできるねん
という事で、こんな仕様にしてみました。
やっとることは至って簡単。
Context#getSystemService(NOTIFICATION_SERVICE)を呼んでNotificationManagerインスタンスを取得してゴニョゴニョするだけです。
メッセージを変える時はもう一度#showNotificationを呼ぶだけです。
でも終了する際にはちゃんと後始末しておきましょうね。
あっ後、フォアグラウンドサービスにする時にはNotificationインスタンスを引数にしてService#startForegroundを呼ぶのですが、この時に一緒にid(0以外の整数)を渡さないといけません。まぁなんでもいいんですが自分は文字列リソースのidを使うことが多いです。
上のコードだと、最初に定義しているNOTIFICATIONフィールド(= R.string.time_shift)がそれです。
もういっぺん状態遷移
前にも載せたけどもういっぺん載せときます。
- STATE_UNINITIALIZED
- STATE_INITIALIZED
- STATE_PREPARING
- STATE_READY
- STATE_BUFFERING
- STATE_RECORDING
- STATE_RELEASING
なんでこないなことにしたかっちゅうと、MediaCodec/MediaMuxerの状態遷移があるからです。MediaCodec/MediaMuxerは融通がきかないので手順から外れると直ぐに例外を投げてきよります。実際にどんな感じに動く(はず)なのかを書いてみましょう。
- クライアント:startService
- サービス :サービスが生成・起動(STATE_UNINITIALIZED)
- クライアント:サービスをバインド要求
- サービス :onBind(STATE_INITIALIZED)
- サービス :ステータスバーに通知を表示してフォアグラウンドサービスへ
- クライアント:映像エンコード準備要求(STATE_PREPARING)
- サービス :ディスクキャッシュ・MediaCodecのエンコーダーを初期化(初期化が終わればSTATE_READY)
- クライアント:タイムシフト開始要求(STATE_BUFFERINGへ)
- クライアント:映像入力用Surfaceを取得
- クライアント:映像入力開始
- サービス :入力された映像をエンコードしてワーカースレッド上でディスクキャッシュへ書き込む
最初のフレームエンコード時にMediaFormatが生成されるので、後ほどMediaMuxerを初期化するときまで保持しておく必要があります。 - クライアント:録画開始要求
- サービス :MediaMuxerを初期化。(別の)ワーカースレッド上でディスクキャッシュからエンコード済みの映像を取り出してMediaMuxerを介してファイルへ書き出す(STATE_RECORDING)
- クライアント:録画終了要求
- サービス :(STATE_BUFFERING)
- クライアント:タイムシフト終了要求
- サービス :(STATE_READY)
- サービス :映像のエンコード・ディスクキャッシュへの書き込みを終了
- クライアント:サービスをアンバインド
- サービス :(STATE_RELEASING)
- サービス :ファイルへの書き込みが終わればstopSelf
- サービス :後始末してフォアグラウンドサービスを解除
多少は前後する場合もあるけど、おおむねこんな感じに実行されます。なげぇよ(。・_・。)
これをサービス内で実行するだけでタイムシフト録画が出来るようになりますキッパリ(笑) 簡単だよね:p)
とりあえず対応するパブリックメソッドを書いてみましょう。
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 |
/** * バッファリングの準備 * @param maxShiftMs 最大シフト時間[ミリ秒] * @param width * @param height * @param frameRate * @param bpp */ public void prepare(final long maxShiftMs, final int width, final int height, final int frameRate, final float bpp); /** * タイムシフトバッファリングを開始 * 実際の所ここでは状態遷移するだけ。エンコード用のスレッドはprepareで生成しとる */ public void start(); /** * タイムシフトバッファリングを終了 */ public void stop(); /** * タイムシフト処理を全て停止して初期状態に戻す * (一度#prepareを呼ぶと#clearを呼ぶまではキャッシュディレクトリやキャッシュサイズは変更できない) */ public void clear(); /** * 録画開始。こっちは内蔵ストレージか外部ストレージ(要パーミッション)へ書き出す時 * @param outputPath */ public void startRecording(final String outputPath); /** * 録画開始。こっちはSDカードへ直接書き出す時(要パーミッション)に使うねん。 * @param accessId */ public void startRecording(final int accessId); /** * 録画終了, バッファリングは継続 */ public void stopRecording(); /** * 映像入力用のSurfaceを取得する * @return */ public Surface getInputSurface(); /** * MediaReaper#frameAvailableSoonを呼ぶためのヘルパーメソッド */ public void frameAvailableSoon(); |
なっ、簡単やろ?普通にMediaCodecを使うときと変わらへん。
違うのはMediaCodecのエンコーダーからエンコード済みのデータを受け取った時に、
- MediaMuxerへ直接書き込むか、
(これは普通のMediaCodex/MediaMuxerの使い方) - TimeShiftDiskCacheへ書き込む→TimeShiftDiskCacheの一番古いものを取り出してMediaMuxerへ書き込む
基本的にはこれだけの違いしかないねん。こんな簡単やったらコード載せんでも自分で作れるやろ?
あかん?
MediaCodecの準備はいつもどおりこんな感じ。いつもと違うのはTimeShiftDiskCache用のキャッシュディレクトリを処理をせなあかん所やな。
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 |
/** * バッファリングの準備 * @param maxShiftMs 最大シフト時間[ミリ秒] * @param width * @param height * @param frameRate * @param bpp * @throws IllegalStateException * @throws IOException */ public void prepare(final long maxShiftMs, final int width, final int height, final int frameRate, final float bpp) throws IllegalStateException, IOException { if (DEBUG) Log.v(TAG, "prepare:"); synchronized (mSync) { if (mState != STATE_INITIALIZED) { throw new IllegalStateException(); } setState(STATE_PREPARING); try { File cacheDir = null; if (!TextUtils.isEmpty(mCacheDir)) { // キャッシュディレクトリが指定されている時 cacheDir = new File(mCacheDir); } if ((cacheDir == null) || !cacheDir.canWrite()) { // キャッシュディレクトリが指定されていないか書き込めない時は外部ストレージのキャッシュディレクトリを試みる cacheDir = getExternalCacheDir(); if ((cacheDir == null) || !cacheDir.canWrite()) { // 内部ストレージのキャッシュディレクトリを試みる cacheDir = getCacheDir(); } } if ((cacheDir == null) || !cacheDir.canWrite()) { throw new IOException("can't write cache dir"); } VideoConfig.maxDuration = maxShiftMs; mVideoCache = TimeShiftDiskCache.open(cacheDir, BuildConfig.VERSION_CODE, 2, mCacheSize, maxShiftMs); createEncoder(width, height, frameRate, bpp); setState(STATE_READY); } catch (final IllegalStateException | IOException e) { releaseEncoder(); releaseCache(); throw e; } } } /** * MediaCodecのエンコーダーを生成 * @param width * @param height * @param frameRate * @param bpp * @throws IOException */ private void createEncoder(final int width, final int height, final int frameRate, final float bpp) throws IOException { if (DEBUG) Log.v(TAG, "createEncoder:"); final MediaCodecInfo codecInfo = selectVideoCodec(MIME_AVC); if (codecInfo == null) { throw new IOException("Unable to find an appropriate codec for " + MIME_AVC); } final MediaFormat format = MediaFormat.createVideoFormat(MIME_AVC, width, height); // MediaCodecに適用するパラメータを設定する。 // 誤った設定をするとMediaCodec#configureが復帰不可能な例外を生成する format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // API >= 18 format.setInteger(MediaFormat.KEY_BIT_RATE, VideoConfig.getBitrate(width, height, frameRate, bpp)); format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // IFrameの間隔は1秒にする if (DEBUG) Log.d(TAG, "format: " + format); // 設定したフォーマットに従ってMediaCodecのエンコーダーを生成する mVideoEncoder = MediaCodec.createEncoderByType(MIME_AVC); mVideoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // エンコーダーへの入力に使うSurfaceを取得する mInputSurface = mVideoEncoder.createInputSurface(); // API >= 18 mVideoEncoder.start(); mVideoReaper = new MediaReaper.VideoReaper(mVideoEncoder, mReaperListener, width, height); } |
下のコードのMediaReaper.ReaperListener#writeSampleDataの中でTimeShiftDiskCacheへ書き込んどります。
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 |
/** * MediaReaperからのコールバックリスナーの実装 */ private final MediaReaper.ReaperListener mReaperListener = new MediaReaper.ReaperListener() { @Override public void writeSampleData(final int reaperType, final ByteBuffer byteBuf, final MediaCodec.BufferInfo bufferInfo) { // if (DEBUG) Log.v(TAG, "writeSampleData:"); if (reaperType == MediaReaper.REAPER_VIDEO) { try { final long pts = getInputPTSUs(); synchronized (mSync) { if (mVideoCache != null) { final TimeShiftDiskCache.Editor editor = mVideoCache.edit(pts); editor.set(0, byteBuf, bufferInfo.offset, bufferInfo.size); editor.set(1, bufferInfo.flags); editor.commit(); } } } catch (final IOException e) { stopAsync(); Log.w(TAG, e); } } } @Override public void onOutputFormatChanged(final MediaFormat format) { if (DEBUG) Log.v(TAG, "onOutputFormatChanged:"); mVideoFormat = format; // そのまま代入するだけでいいんかなぁ } @Override public void onStop() { if (DEBUG) Log.v(TAG, "onStop:"); releaseEncoder(); } @Override public void onError(final Exception e) { if (DEBUG) Log.v(TAG, "onError:"); stopAsync(); Log.w(TAG, e); } }; |
でもって録画開始も普段とたいしてかわらへん。今回はSDカードへの書き込む時のメソッドも書いたからちょっと長ごなっとるけど。
普通に録画する時はエンコードの開始とエンコード済みのフレームデータをMediaMuxerで書く出す処理を同時に開始させるけど、今回はエンコードは既に開始しとるけん、MediaMuxerへの書き出し処理だけを開始すればええねん。
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 |
/** * 録画開始 * @param outputPath */ public void startRecording(final String outputPath) throws IllegalStateException, IOException { if (DEBUG) Log.v(TAG, "startRecording:"); synchronized (mSync) { if (mState != STATE_BUFFERING) { throw new IllegalStateException("not started"); } if (mVideoFormat != null) { if (checkFreeSpace(this, 0)) { final IMuxer muxer = new MediaMuxerWrapper(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); final int trackIndex = muxer.addTrack(mVideoFormat); mRecordingTask = new RecordingTask(muxer, trackIndex); new Thread(mRecordingTask, "RecordingTask").start(); } else { throw new IOException(); } } else { throw new IllegalStateException("there is no MediaFormat received."); } setState(STATE_RECORDING); } } private static final String EXT_VIDEO = ".mp4"; private String mOutputPath; /** * 録画開始 * @param accessId */ public void startRecording(final int accessId) throws IllegalStateException, IOException { if (DEBUG) Log.v(TAG, "startRecording:"); synchronized (mSync) { if (mState != STATE_BUFFERING) { throw new IllegalStateException("not started"); } if (mVideoFormat != null) { if (checkFreeSpace(this, accessId)) { // 録画開始 final IMuxer muxer; if ((accessId > 0) && SDUtils.hasStorageAccess(this, accessId)) { mOutputPath = FileUtils.getCaptureFile(this, Environment.DIRECTORY_MOVIES, null, EXT_VIDEO, accessId).toString(); final String file_name = FileUtils.getDateTimeString() + EXT_VIDEO; final int fd = SDUtils.createStorageFileFD(this, accessId, "*/*", file_name); muxer = new VideoMuxer(fd); } else { // 通常のファイルパスへの出力にフォールバック try { mOutputPath = FileUtils.getCaptureFile(this, Environment.DIRECTORY_MOVIES, null, EXT_VIDEO, 0).toString(); } catch (final Exception e) { throw new IOException("This app has no permission of writing external storage"); } muxer = new MediaMuxerWrapper(mOutputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); } final int trackIndex = muxer.addTrack(mVideoFormat); mRecordingTask = new RecordingTask(muxer, trackIndex); new Thread(mRecordingTask, "RecordingTask").start(); } else { throw new IOException(); } } else { throw new IllegalStateException("there is no MediaFormat received."); } setState(STATE_RECORDING); } } |
実際のMediaMuxerへの書き出し処理はここな。Runnableとして実装して録画開始時にThreadのコンストラクタへ渡してスレッド上で実行してもらうねん。このRecordingTask#runループ内でTimeShiftDiskCacheの一番古いものを取り出してはMediaMuxerへ書き込んでおりまする。
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 |
/** * 非同期でディスクキャッシュからエンコード済みの動画フレームを取得して * mp4ファイルへ書き出すためのRunnable */ private class RecordingTask implements Runnable { private final IMuxer muxer; private final int trackIndex; public RecordingTask(final IMuxer muxer, final int trackIndex) { this.muxer = muxer; this.trackIndex = trackIndex; } @SuppressWarnings("WrongConstant") @Override public void run() { if (DEBUG) Log.v(TAG, "RecordingTask#run"); final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); int frames = 0, error = 0; byte[] buf = null; long prevPts = 0; TimeShiftDiskCache.Snapshot oldest; muxer.start(); boolean iFrame = false; for ( ; ; ) { synchronized (mSync) { if (mState != STATE_RECORDING) break; try { if (mVideoCache.size() > 0) { oldest = mVideoCache.getOldest(); info.size = oldest != null ? oldest.available(0) : 0; if (info.size > 0) { info.presentationTimeUs = oldest.getKey(); buf = oldest.getBytes(0, buf); info.flags = oldest.getInt(1); oldest.close(); mVideoCache.remove(info.presentationTimeUs); if (info.presentationTimeUs == prevPts) { Log.w(TAG, "duplicated frame data"); info.size = 0; } if (!iFrame) { if ((info.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != MediaCodec.BUFFER_FLAG_KEY_FRAME) { continue; } else { iFrame = true; } } } } else { info.size = 0; } } catch (final IOException e) { info.size = 0; } if (info.size == 0) { try { mSync.wait(TIMEOUT_MS); } catch (final InterruptedException e) { break; } continue; } } // synchronized (mSync) if (DEBUG) Log.v(TAG, "writeSampleData:size=" + info.size + ", presentationTimeUs=" + info.presentationTimeUs); try { frames++; muxer.writeSampleData(trackIndex, ByteBuffer.wrap(buf, 0, info.size), info); } catch (final Exception e) { Log.w(TAG, e); error++; } } // for ( ; ; ) try { muxer.stop(); } catch (final Exception e) { Log.w(TAG, e); } try { muxer.release(); } catch (final Exception e) { Log.w(TAG, e); } if (DEBUG) Log.v(TAG, "RecordingTask#run:finished, cnt=" + frames); } } |
ほらな?載せんでもつくれたやろ。
載ってないメソッドやフィールドは名前で想像するか小人さんに手伝ってもらってな^^ もしかしたら夜中にプログラムの女神様が手伝ってくれるかもしれんし(*ノω・*)テヘ
そや、MediaReaperはお初やから載せとこ。MediaCodecのエンコーダーを使う時に、何度もおんなじこと書くのに飽きてきたのでヘルパー用のクラスを作ってん。とは言うても中身は過去に何度も載せとるしGithub上にも晒してあるのをコピペしただけやけどな。
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 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 |
public abstract class MediaReaper implements Runnable { private static final boolean DEBUG = true; // FIXME 実働時はfalseにすること private static final String TAG = MediaReaper.class.getSimpleName(); public static final int REAPER_VIDEO = 0; public static final int REAPER_AUDIO = 1; public static final int TIMEOUT_USEC = 10000; // 10ミリ秒 public interface ReaperListener { public void writeSampleData(final int reaperType, final ByteBuffer byteBuf, final MediaCodec.BufferInfo bufferInfo); public void onOutputFormatChanged(final MediaFormat format); public void onStop(); public void onError(final Exception e); } public static class VideoReaper extends MediaReaper { public static final String MIME_AVC = "video/avc"; private final int mWidth; private final int mHeight; public VideoReaper(final MediaCodec encoder, @NonNull final ReaperListener listener, final int width, final int height) { super(REAPER_VIDEO, encoder, listener); if (DEBUG) Log.v(TAG, "VideoReaper#コンストラクタ"); mWidth = width; mHeight = height; } @Override protected MediaFormat createOutputFormat(final byte[] csd, final int size, final int ix0, final int ix1) { if (DEBUG) Log.v(TAG, "VideoReaper#createOutputFormat"); final MediaFormat outFormat; if (ix0 >= 0) { outFormat = MediaFormat.createVideoFormat(MIME_AVC, mWidth, mHeight); final ByteBuffer csd0 = ByteBuffer.allocateDirect(ix1 - ix0).order(ByteOrder.nativeOrder()); csd0.put(csd, ix0, ix1 - ix0); csd0.flip(); outFormat.setByteBuffer("csd-0", csd0); if (ix1 > ix0) { // FIXME ここのサイズはsize-ix1、今はたまたまix0=0だから大丈夫なのかも final ByteBuffer csd1 = ByteBuffer.allocateDirect(size - ix1 + ix0).order(ByteOrder.nativeOrder()); csd1.put(csd, ix1, size - ix1 + ix0); csd1.flip(); outFormat.setByteBuffer("csd-1", csd1); } } else { throw new RuntimeException("unexpected csd data came."); } return outFormat; } } protected final Object mSync = new Object(); private final WeakReference<mediacodec> mWeakEncoder; private final ReaperListener mListener; private final int mReaperType; /** * エンコード用バッファ */ private MediaCodec.BufferInfo mBufferInfo; // API >= 16(Android4.1.2) private volatile boolean mIsRunning; private volatile boolean mRecorderStarted; private boolean mRequestStop; private int mRequestDrain; private volatile boolean mIsEOS; public MediaReaper(final int trackIndex, final MediaCodec encoder, @NonNull final ReaperListener listener) { if (DEBUG) Log.v(TAG, "コンストラクタ:"); mWeakEncoder = new WeakReference</mediacodec><mediacodec>(encoder); mListener = listener; mReaperType = trackIndex; mBufferInfo = new MediaCodec.BufferInfo(); synchronized (mSync) { // Reaperスレッドを生成 new Thread(this, getClass().getSimpleName()).start(); try { mSync.wait(); // エンコーダースレッド起床待ち } catch (final InterruptedException e) { } } } public void release() { if (DEBUG) Log.v(TAG, "release:"); mRequestStop = true; mIsRunning = false; final MediaCodec encoder = mWeakEncoder.get(); if (encoder != null) { try { encoder.release(); } catch (final Exception e) { Log.w(TAG, e); } } synchronized (mSync) { mSync.notifyAll(); } } public void frameAvailableSoon() { // if (DEBUG) Log.v(TAG, "frameAvailableSoon:"); synchronized (mSync) { if (!mIsRunning || mRequestStop) { return; } mRequestDrain++; mSync.notifyAll(); } } @Override public void run() { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY); // THREAD_PRIORITY_URGENT_AUDIO synchronized (mSync) { mIsRunning = true; mRequestStop = false; mRequestDrain = 0; mSync.notify(); // 起床通知 } boolean localRequestStop; boolean localRequestDrain; for ( ; ; ) { synchronized (mSync) { localRequestStop = mRequestStop; localRequestDrain = (mRequestDrain > 0); if (localRequestDrain) mRequestDrain--; } try { if (localRequestStop) { drain(); mIsEOS = true; release(); break; } if (localRequestDrain) { drain(); } else { synchronized (mSync) { try { mSync.wait(50); } catch (final InterruptedException e) { break; } } } } catch (final IllegalStateException e) { break; } catch (final Exception e) { Log.w(TAG, e); } } // end of while synchronized (mSync) { mRequestStop = true; mIsRunning = false; } } private final void drain() { final MediaCodec encoder = mWeakEncoder.get(); if (encoder == null) return; ByteBuffer[] encoderOutputBuffers; try { encoderOutputBuffers = encoder.getOutputBuffers(); } catch (final IllegalStateException e) { // Log.w(TAG, "drain:", e); return; } int encoderStatus, count = 0; LOOP: for ( ; mIsRunning ; ) { encoderStatus = encoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); // wait for max TIMEOUT_USEC(=10msec) if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { // 出力するデータが無い時は最大でTIMEOUT_USEC x 5 = 50msec経過するかEOSが来るまでループする if (!mIsEOS) { if (++count > 5) break LOOP; // out of while } } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { // if (DEBUG) Log.v(TAG, "INFO_OUTPUT_BUFFERS_CHANGED"); // エンコード時にはこれは来ないはず encoderOutputBuffers = encoder.getOutputBuffers(); } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // if (DEBUG) Log.v(TAG, "INFO_OUTPUT_FORMAT_CHANGED"); // コーデックからの出力フォーマットが変更された時 // エンコード済みバッファの受け取る前にだけ1回来るはず。 // ただし、Android4.3未満だとINFO_OUTPUT_FORMAT_CHANGEDは来ないので // 代わりにflags & MediaCodec.BUFFER_FLAG_CODEC_CONFIGの時に処理しないとだめ if (mRecorderStarted) { // 2回目が来た時はエラー throw new RuntimeException("format changed twice"); } // コーデックからの出力フォーマットを取得してnative側へ引き渡す // getOutputFormatはINFO_OUTPUT_FORMAT_CHANGEDが来た後でないと呼んじゃダメ(クラッシュする) final MediaFormat format = encoder.getOutputFormat(); // API >= 16 if (!callOnFormatChanged(format)) break LOOP; } else if (encoderStatus >= 0) { final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; if (encodedData == null) { // 出力バッファインデックスが来てるのに出力バッファを取得できない・・・無いはずやねんけど throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null"); } if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // if (DEBUG) Log.d(TAG, "drain:BUFFER_FLAG_CODEC_CONFIG"); // Android4.3未満をターゲットにするならここで処理しないと駄目 if (!mRecorderStarted) { // 1回目に来た時だけ処理する // csd-0とcsd-1が同時に来ているはずなので分離してセットする final byte[] tmp = new byte[mBufferInfo.size]; encodedData.position(0); encodedData.get(tmp, mBufferInfo.offset, mBufferInfo.size); encodedData.position(0); final int ix0 = MediaCodecHelper.findStartMarker(tmp, 0); final int ix1 = MediaCodecHelper.findStartMarker(tmp, ix0+1); // if (DEBUG) Log.i(TAG, "ix0=" + ix0 + ",ix1=" + ix1); final MediaFormat outFormat = createOutputFormat(tmp, mBufferInfo.size, ix0, ix1); if (!callOnFormatChanged(outFormat)) break LOOP; } mBufferInfo.size = 0; } if (mBufferInfo.size != 0) { // エンコード済みバッファにデータが入っている時・・・待機カウンタをクリア count = 0; if (!mRecorderStarted) { // でも出力可能になっていない時 // =INFO_OUTPUT_FORMAT_CHANGED/BUFFER_FLAG_CODEC_CONFIGをまだ受け取ってない時 throw new RuntimeException("drain:muxer hasn't started"); } // ファイルに出力(presentationTimeUsを調整) try { mBufferInfo.presentationTimeUs = getNextOutputPTSUs(mBufferInfo.presentationTimeUs); mListener.writeSampleData(mReaperType, encodedData, mBufferInfo); } catch (final TimeoutException e) { // if (DEBUG) Log.v(TAG, "最大録画時間を超えた", e); callOnError(e); } catch (final Exception e) { // if (DEBUG) Log.w(TAG, e); callOnError(e); } } // 出力済みのバッファをエンコーダーに返す encoder.releaseOutputBuffer(encoderStatus, false); if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { // ストリーム終了指示が来た時 callOnStop(); break LOOP; } } } // for ( ; mIsRunning ; ) // if (DEBUG) Log.v(TAG, "drain:finished"); } protected abstract MediaFormat createOutputFormat(final byte[] csd, final int size, final int ix0, final int ix1); private boolean callOnFormatChanged(final MediaFormat format) { try { mListener.onOutputFormatChanged(format); mRecorderStarted = true; return true; } catch (final Exception e) { callOnError(e); } return false; } private void callOnStop() { try { mListener.onStop(); } catch (final Exception e) { callOnError(e); } } private void callOnError(final Exception e) { try { mListener.onError(e); } catch (final Exception e1) { Log.w(TAG, e1); } } /** * 前回出力時のpresentationTimeUs */ private long prevOutputPTSUs = -1; /** * Muxerの今回の書き込み用のpresentationTimeUs値を取得 * @return */ protected long getNextOutputPTSUs(long presentationTimeUs) { if (presentationTimeUs < = prevOutputPTSUs) { presentationTimeUs = prevOutputPTSUs + 9643; } prevOutputPTSUs = presentationTimeUs; return presentationTimeUs; } } |
無駄に長くなってもうたわ(汗) 次も大したことないけどクライアント側を少し載せておしまいにするわ。
という事で今日はさよならサヨナラ(^.^)/~~~