ドキュメントがない機能をどうやって実装するか

こんにちは。 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 さんです。