こんにちは。 iOS/Android エンジニアをやっている @Gemmbu です。 Advent Calendar には毎年参加しているのですが、誰にも記事が刺さらず毎度心がちょっと折れています。
ドキュメントがない機能をどうやって実装するか
iOS 10 から Broadcast Extension という iOS/TvOS 端末の画面を配信する機能があります。 その機能にはドキュメントがありませんでした。(いや、最初は本当になかったんです。いまは多少書かれています)
今回はドキュメントがない手探りの状態でどうやって実装していくかという手法のお話です。 なので、 iOS にも Broadcast Extension にも興味がない方でも読み物として楽しめれば幸いです。
そもそもドキュメントがない?
モバイル開発では Apple 及び Google がドキュメントを整備しているのだからそんなことないのでは? と思った方がいるかもしれませんが残念ながらそんなことはありません。 ドキュメントに書かれていないのだから非公開な API なのでは?と思う方もいるかもしれません。 確かにそういうものもあります。 しかしながら API のインタフェースは公開されていますが、ドキュメントに何も書かれていないものが存在します。
たとえば Android ではこちらの ActivityLifecycleCallbacks
という interface https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks.html
ざっくりいうと Android の画面の状態が変更された通知を受け取ることができるようになるのですが、 API ドキュメントには何も書かれていません。
iOS ではこちらの Broadcast Extension と呼ばれる iPhone の画面をリアルタイム配信する機能のあるクラスです https://developer.apple.com/reference/replaykit/rpbroadcastactivityviewcontroller?language=objc Android と同様に API のインタフェースは公開されていますが、API ドキュメントには何も書かれていません(今は書かれています)。
ドキュメントがない?にも色々ある
ここでドキュメントがない?パターンを整理してみましょう
*ドキュメントがメンテンスされていない
Android の ActivityLifecycleCallbacks
はおそらくこちらのパターンです。
google さんがこうだから許されるということではないのですが、こういうのをみてダメ人間な私は安心します。
- ドキュメントはあるが一般に公開されていない
iOS の Broadcast Extension はこちらのパターンです。 https://developer.apple.com/videos/play/wwdc2016/601/ は主に Broadcast Extension を呼び出す方法について書かれているのですが、 Broadcast Extension の実装については
Work together with us
と書かれており、詳細には触れていません。
- 非公開 API だからドキュメントがない
iOS/Android エンジニアは危険だから知識としてどういうものがあるか蓄えておきましょう。
ドキュメントはあるが一般に公開されていない機能をどう実装していくか
戦略
基本的にいつもこんなことをやっています
- とりあえずやってみる
- ログを無心で追加してみる
- わかっていること、参考になりそうなところはそれをみて埋めて本当にわからないところを明確にする
- 難しそうな問題を簡単に解決できる方法がないか考える
- わかんなくなったら別のことをやってみる
重要なのは本当にわからないところを明確にするというところでしょうか。 あと、 iOS/Android では難しいが mac/PC 上では簡単に解決する問題については積極的にそちらの環境でやってみると効率的です。
プロジェクトを作成し、 Broadcast Extension を追加
とりあえずやってみる系
なんにせよプロジェクトを作らないとはじまらないので、プロジェクトを作成し、 Broadcast Upload Extension を追加しましょう。 自動的に Broadcast UI Extension も追加されます。 さきほどまで Broadcast Extension と呼んでいたのですが、正確には Broadcast Upload Extension と Broadcast UI Extension の二つです。
簡単に説明すると Broadcast Upload Extension は動画配信サービスに動画をアップロードする機能です。 Broadcast UI Extension は配信する際の設定等を行う画面を提供する機能です。
Broadcast Upload Extension と Broadcast UI Extension で追加されたファイルのメソッド呼び出しにログを仕込む
ログを無心で追加してみる系
無心で行います。
// Called when the user has finished interacting with the view controller and a broadcast stream can start - (void)userDidFinishSetup { NSLog(@"%p %s:%d", self, __FUNCTION__, __LINE__); // ログを出力 ...
例えば上記のように NSLog(@"%p %s:%d", self, __FUNCTION__, __LINE__);
と書くことにより、
呼び出されたメソッドのインスタンスのポインタ、関数名、行番号を出力することができます。
重要なのはインスタンスのポインタでこれを確認することにより ライフサイクルを自分で制御できないインスタンスがいつまで生きているか、 同じ API が呼び出されたが同じインスタンスかそうじゃないかがわかります。
例えば Broadcast Upload Extension に含まれる MovieClipHandler
というクラスの - (void)processMP4ClipWithURL:(NSURL *)mp4ClipURL setupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo finished:(BOOL)finished
という
細切れの動画ファイルの URL を通知する関数があるのですが、この API に呼び出し時に先ほどのログを仕込むことにより
インスタンスのポインタが毎回違うことがわかります。
つまり毎回生成され破棄されているということです。 そのため、なにか配信中の状態を保存しておこうと思った際にこのクラスにインスタンス変数を使用すると都度破棄されるのであまりよくないかも?ということに気がつくことができます。 ブレークポイントでやっている場合は個人的にはなかなか気が付きません。
わかっている資料をみて埋められるところは埋める
わかっていること、参考になりそうなところはそれをみて埋めて本当にわからないところを明確にする系
先ほど書いたように https://developer.apple.com/videos/play/wwdc2016/601/ 主に Broadcast Extension を呼び出す方法について書かれているのですが、 呼び出し側のコードをもとにこのように使うのだろうなぁと想像し、コードを追加していきます。 コードだけでわからなくなった場合はスライド内の文章や図、また発表者の発言も逃さず実装のヒントにしましょう。
さらにヘッダファイルを見ましょう。 iOS の API ドキュメントには書かれていない場合でもヘッダファイルには詳細がかかれていることが多々あります。
これを行うことにより、わかっていないところを少しでも減らすことができます。 難しいのは参考にした先の信頼性。 いろんなログを出したり、次の Assert を書くことでここまでは大丈夫ってところを少しづつ広げていきましょう。
Assert を書く
これも、 わかっていること、参考になりそうなところはそれをみて埋めて本当にわからないところを明確にする系
Assert の重要性についてはご存知だと思いますが、知らない API を叩く際にも自分が予期していない状態を表明しておくことは重要です。
http サーバをサクッとたてる
難しそうな問題を簡単に解決できる方法がないか考える系
ドキュメントはないなりに作業を進めていくととうとう本題の動画配信サービスに動画をアップロードする機能にたどり着きます。 ここにきて、この Broadcast Upload Extension で得られる動画ファイルの詳細がわからないことに気がつきます。
具体的には Broadcast Upload Extension に含まれる MovieClipHandler
というクラスの - (void)processMP4ClipWithURL:(NSURL *)mp4ClipURL setupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo finished:(BOOL)finished
から
mp4ClipURL
を送信するのですが、このファイルの仕様が明確にかかれていません。
iOS 内部でファイルの詳細を調査するコードを書くべきでしょうか? そのためには動画ファイルのフォーマットを調べるのがいいのでしょうか? iOS SDK には動画ファイルを判断するクラスがあったでしょうか? 考えるだけで時間がかかってしょうがありません。
それなら送信したファイルを保存する http サーバを立てましょう。 ファイルを外に持っていけば、あとは豊富な mac 側のツール群で確認できます。 どちらにしよ Broadcast Upload Extension は動画配信サービスに動画をアップロードする機能なので、受けて側のサーバが必要になります。
なので私は go でさらさらと http サーバを書いてしまいました。
取得したファイルを確認する
難しそうな問題を簡単に解決できる方法がないか考える系
http から取得したファイルを確認しましょう。
とりあえずは file
コマンドで
$ file 0.mp4 0.mp4: ISO Media, MPEG v4 system, version 2
結果を見る限り、普通の動画ファイルのようですね。
詳細を確認するために ffprobe
コマンドを使用してみると以下のように詳細がわかります。
$ ffprobe 0.mp4 ffprobe version 3.1.4 Copyright (c) 2007-2016 the FFmpeg developers built with Apple LLVM version 8.0.0 (clang-800.0.38) configuration: --prefix=/usr/local/Cellar/ffmpeg/3.1.4 --enable-shared --enable-pthreads --enable-gpl --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-opencl --enable-libx264 --enable-libmp3lame --enable-libxvid --disable-lzma --enable-vda libavutil 55. 28.100 / 55. 28.100 libavcodec 57. 48.101 / 57. 48.101 libavformat 57. 41.100 / 57. 41.100 libavdevice 57. 0.101 / 57. 0.101 libavfilter 6. 47.100 / 6. 47.100 libavresample 3. 0. 0 / 3. 0. 0 libswscale 4. 1.100 / 4. 1.100 libswresample 2. 1.100 / 2. 1.100 libpostproc 54. 0.100 / 54. 0.100 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '0.mp4': Metadata: major_brand : mp42 minor_version : 1 compatible_brands: mp41mp42isom creation_time : 2016-12-02 04:28:36 Duration: 00:00:05.00, start: 0.000000, bitrate: 658 kb/s Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 750x1334, 432 kb/s, 28.99 fps, 30 tbr, 600 tbn, 1200 tbc (default) Metadata: creation_time : 2016-12-02 04:28:36 handler_name : Core Media Video Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 106 kb/s (default) Metadata: creation_time : 2016-12-02 04:28:36 handler_name : Core Media Audio Stream #0:2(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 106 kb/s (default) Metadata: creation_time : 2016-12-02 04:28:36 handler_name : Core Media Audio
配信してみる
難しそうな問題を簡単に解決できる方法がないか考える系
ここまで順調に動けばあとは HLS でも利用して配信するだけです。
http サーバを自分で書いたのでしたら、動画の POST を契機に .m3u8 のファイルを更新し、
ffmpeg
コマンドを利用して mp4 ファイルを ts に変換すればよいでしょう。
と、今回の Broadcast Extension は配信まで確認するだけならそんなに分量もないので半日もかからずできると思いますので 一度挑戦してみるとどうでしょうか?
おまけ
今回は Broadcast Extension の実装についてがメインの話ではないのですが、 実装中に気づいたことが幾つかあるのでメモしておきます。
Broadcast Upload Extension に含まれる SampleHandler は何をするもの?
SampleHandler.m のコメントにかかれているのですが、 MovieClipHandler は mp4 で録画データを取得できるのですがそうではなくフレーム毎の生データを取得できます。 このクラスを用いることで既存の RTMP に乗せたり、独自プロトコルに乗せたりすることができます。 使用するには SampleHandler.m に方法が書かれています。
Broadcast Upload Extension を使って外部配信せずに端末内部に保存することはできる?
AVAssetWriter を用いて SampleHandler.m から取得した生データで動画ファイルを作ることは可能。
配信終了時に呼ばれる SampleHandler の - (void)broadcastFinished
で AVAssetWriter の終了処理を行うと
終了完了処理前に Broadcast Upload Extension のプロセスが終了するので工夫が必要です。
しかしながら App Group を用いてファイルを Broadcast Upload Extension からアプリ側にコピーしようとしても行えない?ので ファイルを簡単に端末でみられるようにはできません。
まとめ
Broadcast Extension の実装を通じてどのような考えのもと試行錯誤しているか書いてみました。 iOS/Android エンジニアでも検証のためにさくっとサーバを書いてみたり、さくっと mac/PC で調べたりできると簡単に問題を解決できることが多々ありますので、いろいろなことに手を出してみるとよいでしょう。 もし、これをみて Broadcast Extension 以外の実装をやってみたいと思ったのでしたら、 https://developer.apple.com/videos/play/wwdc2014/513/ に挑戦してみると面白いと思います。
明日は、@nobiii さんです。