Mirrativ tech blog

株式会社ミラティブの開発者(サーバサイドエンジニア,iOSエンジニア,Androidエンジニア,機械学習エンジニア,インフラエンジニア, etc...)によるブログです

【Unity】Mirrativのアバターがなんで動いているのか誰にもわからないので説明する

こんにちは、よこてです。Mirrativ のアバターは Unity で動いているという話をします。Mirrativ は iOS/Android の ライブ配信アプリですが、機能の一つとしてエモモ(アバター)があります。

f:id:n0mimono:20191203204328p:plain

これは Unity で動いているのですが Mirrativ そのものはネイティブのモバイルアプリです。意味がわかりませんね。具体的には

f:id:n0mimono:20191203204412p:plain

オレンジの部分がネイティブで実装されていて、青い部分がUnityで実装されています。わかりにくいですね。要するに 基本的にはネイティブ実装されていて、アバターの部分だけがUnityで実装されています

このように Mirrativ は ネイティブ実装とUnity実装のハイブリッド構成 になっています。これは歴史的経緯があるのですが、Mirrativ はモバイルのライブ配信サービスとして開発され運用されてきました。最初はアバターという機能はなかったため、普通のiOS/Androidのネイティブアプリだったわけです。その後にサービスグロースの過程において Mirrativ にアバターを導入することにしたのですが、 Mirrativ の既存の資産を活かして市場検証を最速で回したいというモチベーションがありました。このような背景があり、 native app + Unity という構成の導入が検討されました。

ネイティブアプリの資産を活かしつつ Unity を利用する方法はいくつか考えられますが、その一つに WebGLを使う方法があります。アバターとしての機能は WebGL という形で出力して、アプリ側からは WebView 経由で表示させるという方法です。Mirrativ のアバター導入検討時の最初期にはこれを実験したのですが、パフォーマンス上の問題があり断念しました。

今 Mirrativ では Unity をフレームワークとして扱い、ネイティブに埋め込むという Embedded Unity を採用しています。Unity 2019.3 では Unity as a library という形でサポートされる予定ですが、実はそれ以前のバージョンでも同じことができるのです。

Embedding Unity

Unity をネイティブアプリに組み込むにあたり考えるべきことは

  1. Unity の view の切り出し
  2. フレームワーク化(iOS なら .framework 、 Android なら .aar として出力)
  3. ネイティブアプリ(Swift/Kotlin)からの利用

まず重要なのは Unity が出力するプロジェクト は single view application である、ということです。Unity は一つの view を生成して全画面表示しているだけで、この view は iOS では単なるUIView、 Android ではSurfaceViewです。つまり Unity の view はネイティブアプリがもつ他の view と全く同じように扱うことができます。

具体的には、 Unity の iOS ビルド時に export される Xcode プロジェクトは

  • main()が Unity のエンジンを起動、またUIApplicationMain()の呼び出し
  • UnityAppControllerUIApplicationDelegateを実装
  • UnityGetGLView()UIViewを取得

となっていて

  • Unity が通常実行するmain()の中身をネイティブ側のコードから実行(かつUIApplicationMain()を呼ばない)
  • UnityAppControllerをネイティブ側のapplicationに追加
  • UnityGetGLView()で得られる view をネイティブ側のもつ view にinsertSubview

とすれば Unity の view をネイティブな iOS アプリ上で動作させることができます。

ちなみに

int main(int argc, char* argv[])
{
    UnityInitStartupTime();
    @autoreleasepool
    {
        UnityInitTrampoline();
        UnityInitRuntime(argc, argv);

        RegisterMonoModules();
        RegisterFeatures();

        std::signal(SIGPIPE, SIG_IGN);

        UIApplicationMain(argc, argv, nil, [NSString stringWithUTF8String: AppControllerClassName]);
    }

    return 0;
}

Unity のmain()はこんな感じで実装されています。

iOS

ビルド

iOS のフレームワーク化はjiulongw/swift-unityの実装がほぼすべてなのですが簡単に説明していきます。

Unity が export する Xcode のプロジェクトはこのような構成になります。この中で特に重要なのは次の3つで

  • Classes
    • Unity のエンジンに相当
    • Classes/UnityAppController.mmUIApplicationDelegate相当
    • Classes/main.mmが entry point 相当
    • これを外部から利用できる形にコードを修正する
  • Libraries
    • 主に .NET の周辺
  • Data
    • アプリに embed されるデータ群

基本的には bridging header を追加してこれらを framework としてビルドするだけです。 .framework が得られるため、他の iOS アプリからフレームワークとして利用することができます。

ネイティブからの利用

ネイティブ側からの利用については、特に重要な部分を抜粋すると

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    @objc var currentUnityController: UnityAppController!

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {        
        unity_init(CommandLine.argc, CommandLine.unsafeArgv)
        
        currentUnityController = UnityAppController()
        currentUnityController.application(application, didFinishLaunchingWithOptions: launchOptions)
        
        currentUnityController.applicationDidBecomeActive(application!)
        currentUnityController.applicationWillResignActive(application!)

        return true
    }

    func applicationWillResignActive(_ application: UIApplication) {
        currentUnityController.applicationWillResignActive(application)
    }
    
    func applicationDidBecomeActive(_ application: UIApplication) {
        currentUnityController.applicationDidBecomeActive(application)
    }
}

unity_init()main()の置き換えになります。unity_init()でエンジンを起動して、UnityAppControllerUIApplicationDelegateとして登録しています。applicationDidBecomeActiveを一度呼び出しているのは、UnityAppControllerapplicationDidBecomeActive時点でグラフィックの初期化(= view の初期化)を行っているためで、明示的にこれを実行しています。

また、view の利用は

class ViewController: UIViewController {
    func showUnitySubView() {
        if let unityView = UnityGetGLView() {
            view?.insertSubview(unityView, at: 0)
            
            unityView.translatesAutoresizingMaskIntoConstraints = false
            let views = ["view": unityView]
            let w = NSLayoutConstraint.constraints(withVisualFormat: "|-0-[view]-0-|", options: [], metrics: nil, views: views)
            let h = NSLayoutConstraint.constraints(withVisualFormat: "V:|-75-[view]-0-|", options: [], metrics: nil, views: views)
            view.addConstraints(w + h)
        }
    }
}

適当なUIViewControllerinsertSubviewすることを考えます。UnityGetGLView()でもらえるUIViewinsertSubviewするだけです。実際にはapplicationDidBecomeActiveから呼び出されるグラフィックの初期化を待った上でshowUnitySubViewを呼び出す必要があります。

メッセージング

ネイティブから Unity に対してはSendMessageを利用できます。

void UnitySendMessage(const char* obj, const char* method, const char* msg);

Unity からネイティブに対しては native plugin を利用できます。文字列を渡すだけなら

#ifndef UnityPlayerToIOS_h
#define UnityPlayerToIOS_h

@protocol UnityCallback <NSObject>
@optional
@required
- (void)receiveMessage: (NSString *)msg;
@end

@interface UnityPlayerToIOS : NSObject
@property (class, nonatomic) id <UnityCallback> receiver;
+ (void)sendMessage: (NSString *)msg;

@property (class, nonatomic) id <UnityCallback> callbackReceiver;
+ (void)sendCallbackMessage: (NSString *)msg;
@end
#endif
#import "UnityPlayerToIOS.h"

static id <UnityCallback> _receiver = nil;

@implementation UnityPlayerToIOS
+ (id <UnityCallback>) receiver {
    return _receiver;
}
+ (void)setReceiver: (id <UnityCallback>)receiver {
    _receiver = receiver;
}
+ (void)sendMessage: (NSString *)msg {
    if (_receiver) {
        [_receiver receiveMessage:msg];
    }
}
@end

extern "C" {
    void sendMessage(const char *msg) {
        [UnityPlayerToIOS sendMessage:[NSString stringWithCString: msg encoding:NSUTF8StringEncoding]];
    }
}

Unity からは文字列を流し込み、ネイティブ側はコールバックを受け取るようにします。

これら2つで双方向にテキストを流せるため、ネイティブからの呼び出しという形で Unity 側のAPIを構築することができます。

Android

ビルド

こちらの記事がほぼすべてになります。Android のライブラリ作成は iOS と比べて簡単で、 AndroidManifest.xml と build.gradle を書き換えるだけでライブラリを作ることができます。

AndroidManifest.xml から applicationタグを消します。

  <!--
  <application android:icon="@mipmap/app_icon" android:label="@string/app_name" android:isGame="true" android:banner="@drawable/app_banner" android:theme="@style/UnityThemeSelector.Translucent">
    ...
  </application>
  -->

build.gradle をlibraryに対応します。

apply plugin: 'com.android.library'

これだけです。あとはメニューからビルドをポチっとするだけで .aar が作成されます。

ネイティブからの利用

Unity のライブラリにはエンジンをラップする形でUnityPlayerという view があり、UnityPlayerActivityがこれをもっています。UnityPlayerを使う最も手っ取り早い方法はUnityPlayerActivityを継承してしまうことで

class UnityActivity : UnityPlayerActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_unity)

        frame.addView(mUnityPlayer)
    }
}

これだけで Unity を埋め込んだ形で使うことができます。ただし、 iOS では view を生成する部分のコードが直接出力されるため view の不透明/半透明を切り替えるのはそれほど難しくありませんが、Android で同じことをやろうとすると多少の工夫が必要になります。UnityPlayerの実体はSurfaceViewですがこれに直接触る手段がないため

        var view = mUnityPlayer.view
        var field = view.javaClass.getDeclaredField("s")
        field.isAccessible = true
        var sf = field.get(view) as SurfaceView
        sf.setZOrderOnTop(true)
        sf.holder.setFormat(PixelFormat.TRANSPARENT)

このようにリフレクションでSurfaceViewを引っ張ってくる必要があります(フィールド名はUnityのバージョンによって変わります)。

メッセージング

iOS と同様にネイティブから Unity に渡すときはSendMessage

UnityPlayer.UnitySendMessage(gameObject, method, msg)

Unity からネイティブにわたすときはコールバックを使います。

public class UnityPlayerToAndroid {
    public interface ICallback {
        public void receiveMessage(String message);
    }

    public static ICallback receiver = null;

    public void sendMessage(String message) {
        if (receiver != null) {
            receiver.receiveMessage(message);
        }
    }

    public static ICallback callbackReceiver = null;

    public void sendCallbackMessage(String message) {
        if (callbackReceiver != null) {
            callbackReceiver.receiveMessage(message);
        }
    }
}

おわりに

だいたいこんな感じで iOS/Android のネイティブアプリに Unity を組み込むことができます。

現実の運用ではCDが必要になりますが、 Cloud Buildのような便利ツールはないので自前で作ります。 正直、ビルドする方法探すよりこっちのほうが骨が折れます。 またフレームワーク単体だとUnityエディタ上で動作確認が一切行えないため、開発用のシーンを別途作ってあるのですが、フレームワーク自体に操作するためのUIがないためこちらもそれなりに作り込む必要があります。

Unity をネイティブアプリのフレームワークとして扱うことが有用なケースとしては、アプリのコアな機能がネイティブ側にある場合で 3D を扱う必要がある場合には検討しても良いのではないでしょうか。 3D である程度リッチな機能を高速につくろうとすると Unity が圧倒的に優秀な一方、 Unity はあくまでゲームエンジンなのでネイティブアプリのような構成でつくるのは必ずしも得意というわけではないからです。 Mirrativ はこれに相当します。ただし、開発・運用は複雑になり細かいハマりポイントも多くあるためあまり安易に導入するのは現状としてはオススメできません。

今後の展望としては 2019.3 でサポートされる形への乗り換えを考えています。単純に Unity のバージョンを最新に保ちたいというのが理由ですが、現状 Unity のバージョンを更新するたびに 微妙な不具合が毎回出る (普通にビルドした場合は起きない)というのが観測されていてつらいなーと思っています。そういうわけで、公式がサポートしてくれるのは 仕事がなくなって嬉しい 心強いなーと思っています。

We are hiring!

ミラティブでは Unity を hack しながらエモモを一緒に作ってくれるエンジニアを募集中です! 体験入社という制度もあるのでお気軽にどうぞ!

www.mirrativ.co.jp

www.wantedly.com

【MPTCP】ライブ配信の通信安定化に向けて MultiPath TCP を試験導入している話

こんにちは ハタ です。
今回は Mirrativ の本番サーバの一部に試験導入している
MultiPath TCP (MPTCP) について紹介させていただきたいなと思います。

MultiPath TCP といえば、iOSの Siri で利用していることなどで一時有名になりました
今回紹介するMPTCPも同じ技術を使っており、通信の安定化に向けて取り組んでいる事項の紹介になります

MPTCP の概要と各OSの実装について

f:id:octu0:20191126175452p:plain
MPTCPのイメージ

MultiPath TCP (以降 MPTCP)は、複数の経路を通じて同じホストに対して通信が行えるTCP拡張です。
従来のTCP通信では、単一の通信パスしか使えなかったものが、複数の通信パスを利用できるようになります。
例えばスマートフォンでは 4G 回線と WiFi ネットワークが用意されているため、それぞれから同一のコネクション張り、どちらか片方の経路でコネクションが切れた場合でも、もう片方の経路で通信が維持できる仕組みです。

MPTCPはクライアント側では iOS で利用可能となっていて、残念ながらAndroid での利用はできないようです(いちおう ルート化されたものであればいけそうですがAndroid 4.x なので少し古い)
また、サーバ側では 各種Linuxディストロ 用のKernelを使うことで利用可能です(BSDは FreeBSD のものがありそうです)

今回は iOS 中心に紹介させていただきます。

MultiPath TCP 導入の目的

f:id:octu0:20191125145612g:plain
WiFi <-> 4G の切り替わり

ミラティブではライブ配信の安定化向上を日頃から行っており、MPTCPの試験導入も通信の安定化を目的に検証を進めています。

これまで、「配信が行えない」ことや「配信が止まってしまう」という声を頂いており、様々な理由で配信が正常に行えない事象があることは確認しているのですが
その中の一部の状況を調査してみると、 WiFi と 4G の頻繁な切り替えが発生し配信が不安定になっているという状況などが確認できてきました。

MPTCPの特性上、複数の経路で通信が行えるようになるため、副次的な効果として

  • WiFi <-> 4G が混在する環境のにおいてMPTCPを利用することでスムーズに通信ネットワークの切り替えが行えるようになり、ネットワーク切り替え時による通信断がなくなり配信を継続できるようになるのではないか
  • 上記に伴い WiFi <-> 4G の切替時に発生する通信の切断・接続がなくなり視聴プレーヤー側のリロードがなくなるため視聴体験の向上があるのではないか

などの効果が見込めるものとして検証を進めました。

MPTCPに対応したサーバ

ミラティブでの既存の配信サーバはTCP通信でライブ映像が配信されているため、配信サーバの前段にMPTCPサーバを設置し配信された映像/音声データを中継するように実装しました。

f:id:octu0:20191126175518p:plain
MPTCP サーバの概要

MPTCP は特殊なプロトコルではなく通常の TCP 通信も利用できるため、既存のサーバを MPTCP に対応した kernel 置き換えることも可能だったのですが、下記理由で既存のサーバとは分けてMPTCPのサーバを用意しています。

  • クライアント・サーバ間の通信よりもサーバ・サーバ間の通信は安定しているので、よりユーザさんに近い前段に設置して通信をリレーする
  • 問題がおきたときの切り分けのため
  • 今後の拡張性のため

サーバのOSについては、当時比較的新しい linux kernel 4.19.55 の Ubuntu での実装が利用できたため、Ubuntu 18.04 を選択しています
通信のリレー部分は、アプリケーションレベルでリレーの実装を行いました。(アプリケーションは難しいものではないため今回は割愛します)

また、利用している MPTCP のバージョンは v0.95 または v0.94.0(kernel 4.14.24) を利用しました。

$ sudo dmesg | grep MPTCP
[    0.582045] MPTCP: Stable release v0.95

ちなみに、MPTCPが有効であれば www.multipath-tcp.orgcurl でアクセスすると下記のようなメッセージが表示され、有効であることがわかります

$ curl www.multipath-tcp.org
Yay, you are MPTCP-capable! You can now rest in peace.

逆に MPTCP が利用できない場合は下記のように表示されます

$ curl www.multipath-tcp.org
Nay, Nay, Nay, your have an old computer that does not speak MPTCP. Shame on you!

iOS での実装について

iOS の実装ですが、iOS11 から API が公開され利用できるようになりました(WWDC 2017、iOS 12からはURLSession以外でも Network Framework で利用可能となっています)
また、MPTCPの動作確認をする際は Multipath entitlements が有効である必要があります。

Network frameworkを使っているため iOS12 以降となってしまいますが、multipathの通信が行えているかの確認は下記のように動作確認することができます
(www.multipath-tcp.orgcurl の User-Agent に偽装することで、サーバ上から確認したときと同じく Yay, you are MPTCP-capable! かどうかがわかるメッセージが取得できます)

import Network

let queue = DispatchQueue.global()
let port = NWEndpoint.Port(rawValue: 80)
let parameter = NWParameters.tcp
parameter.multipathServiceType = .handover
let connection = NWConnection(host: "www.multipath-tcp.org", port: port!, using: parameter)
connection.stateUpdateHandler = { state in
  if state == .ready {
    let message = "GET / HTTP/1.1\nHost: www.multipath-tcp.org\nUser-Agent: curl/7.54.0\n\n"
    let data = message.data(using: .utf8)!
    connection.send(content: data, completion: .contentProcessed { error in
      if let error = error {
        print("\(error)")
      }
    })
  }
}
connection.receive(minimumIncompleteLength: 0, maximumLength: 1024 * 1024 * 10) { (data, _, _, error) in
  if let data = data {
    let text = String(data: data, encoding: .utf8)!
    print("message = \(text)")
  } else {
    print("\(#function), err")
  }
}
connection.start(queue: queue)

Network Framework でのTCP通信のプログラムについては今回は割愛しますが、ライブ配信の通信部分はNetwork.Frameworkに切り替えて実装しました

NWParameters.MultipathServiceType

MPTCP の挙動を設定するパラメータとして NWParameters.MultipathServiceType には、handover / interactive / aggregate / disabled の4つの挙動を設定することができるようです

disabled を除くそれぞれのモードでの挙動は、ドキュメントを確認していただきたいのですが、MPTCP を利用して複数のインタフェースで通信できるモードは aggregate だけとなっていて、開発者モードでしか使えないようになっているようです

f:id:octu0:20191126001716p:plain
設定 → デベロッパ → NETWORKINGにあるチェックを有効にしないと aggregate は使えなかった

ミラティブでは、QAを通じて 一番違和感が少なくネットワークの切り替えができた handover モードを利用しています、
環境や状況によるかと思いますが interactive モードではどうしても 4G/LTE の接続にコネクションが残りがちのようで、WiFi 復帰時の体感がより違和感の少ない handover モード を利用することにしました。

また、iOS9 から導入された WiFi アシストの挙動によっては MPTCP が無効になるパターンもあるようです、こちらも今回は細かく取り上げないのでドキュメントを参照してください

MPTCPの通信の中身

実際に MPTCP の通信が行えているかは、MPTCPサーバ上で tcpdump を使い TCP オプションフィールドで確認できます。

07:27:20.757072 IP C > S: Flags [S], seq 3481042045, win 65535,
                          options [mss 1460,nop,wscale 6,mptcp capable {0x19fc1a796a193012},nop,nop,TS val 134990811 ecr 0,sackOK,eol], length 0
07:27:20.757150 IP S > C: Flags [S.], seq 999693183, ack 3481042046, win 27760,
                          options [mss 1420,sackOK,TS val 1195317422 ecr 134990811,nop,wscale 7,mptcp capable csum {0xbec75f1aee94529}], length 0

(CはClient、SはServerになっています、また一部見やすいように改行しています)

SYN および SYN/ACK 時に mptcp capable のオプションが付いていて通信が開始されることがわかります。
MPTCPの通信は通信の中身そのものよりも、実際に通信の切り替えの挙動を見ていただくのがわかりやすいので、MPTCPを有効にした配信の録画を見ていただければなと思います。

f:id:octu0:20191126170347g:plain
配信画面の録画(アーカイブ)のため画面そのものを20倍速にしたもので一部カットしています(画質悪くすいません...)ちなみに表示しているページは こちら です

ステータスが変化した場合でも配信が止まらずに映像が届いていることがわかり、視聴しているプレーヤーでも映像の再取得がされることなく、途切れずに配信がみれました

導入結果

その後さらに色々なパターンでの配信の検証を進めたところ

  • iOS 12/iOS 13における配信中のリトライ(再接続処理)回数が0になる ことがわかりました
  • 一方で視聴体験の向上はMPTCPあり/なしに関わらず解像度を上げる必要があることがわかりました

MPTCPを使うことで通常の配信に比べて通信の安定化に寄与していることがわかったものの 視聴体験の向上については、リロード回数を指標値にしようと思っていたものの有意な差があるわけではなさそうで、体験が悪化している減少について解像度を上げる必要がありそうでした。

とはいえ、MPTCPには期待していたどおりの効果があり、評価できそうで今後の全体に展開するかの検討材料にできそうです

その他今後の展望など

MPTCP の実装は Network.Framework の登場により実装が楽になったものの、インフラ基盤での高可用性をもった構成はもう少しノウハウが必要そうです、特にLBとの相性を考えたときに こちらの記事 にあるようにひと工夫加えてあげるとより可用性を高められそうです
今回のバージョンではまだここまでは出来ていないため今後バージョンアップを重ねたいと思っています

他にも配信だけではなく、視聴側でも MPTCP を利用することで、より途切れにくく配信を見ることができるのでより良いUXを作ることができる可能性もありそうなので、調査を深めても良さそうだなと思っています

We are hiring!

MPTCP などの技術を使ってライブ配信の安定化を一緒に作ってくれるエンジニアを募集中です! 体験入社という制度もあるのでお気軽にどうぞ!

www.mirrativ.co.jp

【保存版】スタートアップがユーザビリティテストを3日ですべて終わらす方法

こんにちは、分析チームの坂本です。 テックブログ3回めの登場です。

ミラティブの分析チームは、もちろんBigQueryや機械学習を用いた定量的なデータ分析も行いますが、ユーザーを理解するための定性分析も行う「分析チーム」であることを掲げています。

※最近では、データアナリストとUXリサーチャーの境界が溶けつつあるという議論があると思いますが、弊社もその流れを採用しています。

突然ですが、ユーザビリティテスト = お金と時間がかかる という認識の方もいらっしゃると思います。 ミラティブでは、ユーザビリティテストをやろう!と言ってから3日間&謝礼実費のみでユーザビリティテストが完了しました(被験者3名)。 どんなことをやったのかという内容を書きながら、ハマりどころををシェアしようと思います。

ユーザビリティテストの一般論

まずは、ユーザビリティテストってなんやねんという話を書いて行きます。といいつつ、ここは巨人の肩に立たせていただきます(笑)。

ユーザビリティテストとはなんなのか?だったり、なんの目的で実施するのかなどは、グッドパッチさんの下記のブログが詳しいので、ぜひ読んでみてください。

goodpatch.com

ミラティブでは、ユーザビリティテストを実施する前に、ミラティブで言うとどういうことなの?的な記事をコンフルにしたためました。

f:id:sakamoto10423:20190714183346p:plain
社内に投稿したユーザビリティテストの記事

ミラティブでユーザビリティテストが必要だった背景

ユーザビリティテストってどれくらいの頻度で実施したら良いのか?という質問がよくあります。理想的には週一くらいのスパンで定期的にやるのが良いと思います。

しかし、スタートアップにはそんな時間も余裕もないわけです。オススメとしては、

  • 大きなリニューアル前に改善の方向性を確かめるために実施
  • 大きなリニューアルのプロトができたときに、プロトタイプで実施

です。反対に、小さな変更であれば、ABテストやデータ分析で検証することが望ましいと考えています。大きな変更は、画面の構造や印象が大きく変わると同時に、コードの変更も大きく入ります。なので、小さく失敗するためにユーザビリティテストを行うのがおすすめです。大きくリリースして、「やっぱり仮説が間違っていた」となると、相当のコードが無駄になります。

Mirrativ は、直近1年間で様々な機能追加をしてきました。エモモ・ガチャ・ギフト・コイン・オーブ・ジェスチャーなどなど、は直近1年間で追加された機能です。それらがスモールテストとともに導入されたこともあり、UI/UX上わかりづらいところが散見されるようになってきました。

f:id:sakamoto10423:20190714200502p:plain
UI/UX上わかりづらいメニューの例

たとえば、上記であればヘッダメニューの中に

  • 配信一覧のフィルタを変更する、タブ切り替え
  • 自身のエモモ(アバター)を切り替える

という機能が混在しています。 現在ミラティブでは、このような使いづらい箇所を大きく手直しするプロジェクトが始まっています。そのインプットの一環として、ユーザビリティテストを行ったというのが背景です。

事前準備のTips

ここからがこのブログの本編パートになります。もっといいやり方あるよーってあれば、ぜひTwitterやはてブなどでコメントいただけると嬉しいです。

中継用のカメラはMacのカメラで十分

まず、ユーザビリティテストでほぼ必須なのが中継用のカメラです。被験者の手元を写しながら、その映像を別室に中継するためです。

PCサービスの場合は、テレビ会議システムの画面共有機能で十分だと思います。しかし、Mirrativ はモバイルアプリなので、被験者の手の動きを撮影することが必須です。そのため、何かしらの撮影機材が必要でした。

f:id:sakamoto10423:20190722111801p:plain
Macのカメラでユーザビリティテストを行う

いろいろと試した結果、上記のようにMacを後ろから抱きかかえるようなスタイルで撮影したところ、うまく撮ることができました。その映像をテレビに映したものを写真に撮ったものが下記です。もろもろ経由しているので、下記の写真の解像度はそんなに高くないのですが、テストを行うのに必要十分な画質でした。

f:id:sakamoto10423:20190721130748p:plain
Macで動画撮影した映像をテレビに映す様子

カメラのセッティング時に注意すること

カメラが安価で済んだぞ!と意気揚々としていたのですが、実際のテストを行ったときにちょっとしたトラブルがありました。それは、「部屋の調光によっては、蛍光灯の光が画面に反射してしまう」ということです。

f:id:sakamoto10423:20190717014737p:plain
蛍光灯が反射してしまう

我々は最近、自然光あふれる目黒のオフィスに引っ越しを行いましたが、ユーザビリティテストを行ったのは渋谷の旧オフィスの窓がない会議室でした。そのため、蛍光灯の明かりがばっちりスマートフォンに映ってしまいました。我々は一度、被験者さんに承諾をとった上で、(電気を消して)真っ暗な部屋で実施していただきました。

今後の学びとして、蛍光灯にカバーがついている部屋でテストを実施する・自然光が入ってくる部屋で実施するなどの工夫が必要だと感じました。

配信はGoogleHangoutがよかった

では、次はその映像を中継する方法です。Macのカメラで撮影しているので、ビデオ会議システムに通すと楽ですね。そこで下記の条件を満たすビデオ会議システムを探しました。

  • Macのカメラから受けた映像を共有(配信)することができる
  • できれば高画質
  • 40分以上共有(配信)を行うことができる
  • 録画をすることができる
  • 追加料金なし

zoomなどのオンラインミーティングなどをいろいろと検討しましたが、最終的にはGoogleHangout(Google Meet)を選択しました。ただ、Google Meetで録画をするには、G Suite Enterprise (1ユーザー3,000円/月) のプランの契約が必要だったので、Google Meetで録画することは諦めました。

ちなみに今回は、Google Meetで共有した映像をQuickTimeで画面録画するという手法を行ってみましたが、1ファイルが20GBくらいになり、あとあと管理に困りました。これは要改善ポイントです。

被験者の集め方

では、被験者はどうやって集めるのが早いでしょうか?民間のリサーチ会社さんにお願いすると、やはり(やり取りも含めて)1週間程度かかります。そこで、ミラティブでは社員の友人の方に協力いただくことにしました。

そこで、「募集をするなら黄色に黒!」という定番をリスペクトさせていただき、募集画像をつくり社内のSlackに投下しました。するとその日中に目的にあった被験者の方を3名集めることが出来ました。

f:id:sakamoto10423:20190721132735p:plain
ユーザビリティテストの被験者を社内募集

ミラティブ社では、ユーザビリティテストを行うのがはじめて(少なくとも会社独立後ははじめて)だったので、ユーザビリティテストの意義や成果をメンバーが熟知している状態ではありませんでした。そのため、なるべくみなさまに協力いただけるように、目立つ方法で集客しました。

当日の事前注意 & アイスブレイクの工夫

さて、ここからは当日の話を書いていきます。

ユーザビリティテストは、被験者の方にプロダクトを操作していただくテストです。被験者の方は「自分がテストされている」気持ちになってしまうことがある、と事前に知っていたため、下記のようなスライドを用意し、ご自身のテストではないということをかなり強調しました。

f:id:sakamoto10423:20190721135228p:plain
ユーザビリティテストスライド:はじめに#1

f:id:sakamoto10423:20190721135331p:plain
ユーザビリティテストスライド:はじめに#2

また、アイスブレイクでは「普段どんなゲームをされていますか?」というMirrativ と関係ある内容でアイスブレイク出来たのはよかったです。※例えば、ウイルス対策ソフトなどのユーザビリティテストでは「ウイルス対策って普段されますか?」ってのは本題に近すぎて、アイスブレイクには使いづらい質問だと思います。そういうときは、「オフィスまで迷わずに来れました?」みたいなアイスブレイクが一般的なようです。

当日あたふたしたこと

当日のトラブルというか、あわわわってなったことをいくつか紹介します。Mirrativ ならではのこともありますが、「個々のアプリで考えることがある」という意味で共有しておきます。

配信をして頂く際、やめ時が難しい

f:id:sakamoto10423:20190721140811p:plain
ユーザビリティテストスライド:シナリオ#3

3つ目のタスクとして、上記のようなタスクを行ってもらいました。もう少し具体的に言うと、普段プレイしているゲームを配信してもらうというタスクです。

これはインタビュワーが「では終了してください」と声を掛けるタイミングが難しかったです。というのは、ゲームによっては途中で辞めるとスコアが下がったり、たまたまその時間にオンしているマルチフレンドと協力プレイが始まったり 、そもそもの1試合が長かったり。様々な理由で、ゲームのやめ時が読めません。

パズドラやバンドリ!などは比較的短いほうかなと思いますが、マルチ対戦TPSゲームや麻雀ゲームは1試合が30分程度かかってしまうので切り時が難しいなと思います。

ということで、次回はゲームをプレイされる前に、「今回はテストなので、10分程度で一区切りできるゲームを選択いただけませんか?」とアナウンスするのがよいかなと思いました。

想定外の箇所で詰みが発生する

事前の想定外のところで詰みが発生することがあり、インタビュワーが被験者さんにお声がけすることがありました。その一例を紹介します。

例えばiOSで配信する場合、Mirrativ のコメントや情報(「視聴者が来ました」や「ギフトをもらいました」)はPUSH通知を通じて配信者に知らされます。

f:id:sakamoto10423:20190721143948p:plain
配信者はPUSH通知でMirrativ のコメントなどを知る

ということは、PUSH通知を切っていると、視聴者さんが来たとか、視聴者さんがコメントしたことがゲーム中に全くわからないわけです。そのことにはじめて気づきました。なので、2人めの被験者以降は、配信を始める前にPUSH通知をONにしていただく設定をしていただくことにしました。

当日の様子

f:id:sakamoto10423:20190714182759j:plain
ユーザビリティテスト当日の様子

上記はユーザビリティテスト当日のエンジニアルームです。エンジニアルームにある大きなテレビに大きくテスト中の内容を映し、エンジニア・デザイナー・PM全員で注目して見ていました。やはり、自分たちが作成したプロダクトなので、注目度は高かったです。このように、エンジニアチームを巻き込んでテストを行うのが大切ですね。

得られた成果

3名で実施したユーザビリティテストの結果を資料にまとめました。一部公開しておきます。

f:id:sakamoto10423:20190721150119p:plain
得られた成果スライド

上のような資料にまとめるのも重要ですが、それ以上にエンジニアチーム全員がテストの内容を見ていたことのほうが成果としては大きかったかなと思います。やはり、百聞は一見に如かずですね。

さいごに

ミラティブでは分析チームを募集しています。BigQueryや機械学習を使った分析をすることもあれば、今回のような定性的な分析をしていることもあります!プロダクトに近い分析チームです!

◆アナリストポジション www.wantedly.com

◆分析基盤ポジション www.wantedly.com

業務内容が多岐にわたるので、定性分析やったことないわー、とかだったり、定量分析やったことないわーという方も全然OKです!気軽にポチッとエントリーください。

reviewdog x perlcritic x Jenkins で最高の GitHub レビューライフ

ミラティブのサーバーサイドエンジニア、ハトネコエです!

今日は、GitHub の自動レビューとして reviewdog を導入した話をします。

1. 動機

すでに CTO の夏さんによって、Perl 用の linter である perlcritic が導入されていました。
そして、テストが走る際に perlcritic のチェックもおこなわれ、
指摘箇所があればテストが落ちるようになっていました。

まずは緩めの設定で導入したけれど、perlcritic のチェックをもっと厳しくしたい!
だけど厳しくすると、すでに存在するコード(つまり、プルリクで変更していない部分)が原因で
テストが落ちるようになってしまいます。

残念ながら perlcritic には autofix の機能も無いようで、
設定を変更した後は、人力での修正が終わるまでテストは落ち続けてしまいます。

これでは開発に支障が出るので、
テストでのチェックはやめて、プルリクで変更した部分にだけ指摘がなされる手段が求められました。

2. Danger か reviewdog か

最初に話題に上がったのは Danger でした。

f:id:nekonenene:20190706005348p:plain
CTOの夏さんとの会話(in 私のtimes)

Danger は自動レビューの手段として良いツールです。

Android, iOS, Ruby, JavaScript といった Danger のプラグインが存在するコードベースであれば良い選択でしょう。
しかし今回は Perl。Danger のプラグインは存在しません。
作ってもいいですが、あまり時間をかけたくもありません。

そこで見つけたのが reviewdog です。

reviewdogによるGoのコードレビュー - DeNA Testing Blog

こちらのブログがきっかけになりました。ありがとうございます。

3. reviewdog を選んだ理由

3-1. 比較

両者を比べると以下の違いがありました。

reviewdog Danger
スター数 (2019/07/13) 979 3573
コミット頻度 高い 高い
言語 Go Ruby
コメント 修正すべき行にコメント まとまった1つのコメント
(or 修正すべき行にコメント*1
ドキュメント README.md で充分わかる 情報が散乱していて読みにくい

ドキュメントの読みやすさは主観ですが、
Danger のドキュメントの読みにくさは伝わる……はず……!

reviewdog の方がスター数は少ないですが、
それを補うほど優れた点が複数ありました。

3-2. reviewdog は Golang

まず Go 言語であることに惹かれました。
というのも、最近は bundle install 時に起こるエラーに辟易していて、
できれば Gemfile の管理はしたくないな……と思っていたためです。
bundler のバージョンが v2 になってから、まだ落ち着いていないようにも思えますし。

Go はもう少し単純で、かつ v1.13 でよりモジュール管理が簡潔になる予定です
その上、シングルバイナリでアプリケーションを配布することが可能です。

reviewdog もバイナリを配布していますので、
使用者は Go のインストールをすることなく reviewdog を扱えます。

Danger の場合は ruby(&たいていは rbenv)のインストールが必要になりますが、それらが不要なわけです!
reviewdog のバイナリファイルをダウンロードしてくるだけでいいので、
Docker や Jenkins での実行準備がとても簡単におこなえます。

3-3. reviewdog の errorformat が最高!

もし Danger のプラグインを書くとするなら、GitHub コメントを作成するために、
以下のようにいい感じに lint エラーを整形してあげる必要があります。
https://github.com/loadsmart/danger-android_lint/blob/0.0.7/lib/android_lint/plugin.rb#L134-L157
これはなかなかに面倒です。

一方、reviewdog の場合、
errorformat が内蔵されていることにより、
lint エラーのメッセージ形式を渡してあげれば、あとはよしなに解釈して、修正すべき行にコメントを残してくれます。

reviewdog はとても簡単なのです!

perlcritic のような、Danger のプラグインがない linter を使っているのであれば、
reviewdog はとてもいい選択肢に思えます。

4. reviewdog を使ってみる

reviewdog の使い方については、前述したとおり本家の README.md で十分理解できますが、
ここでもいくらか書いておきます。

4-1. -reporter=github-pr-review

reporter オプションの選択肢は複数ありますが、GitHub を使っているのであれば
github-pr-review reporter を選ぶのが良いでしょう。

まず local reporter は、動作テスト用です。
local reporter の場合のみ、 diff オプションが必要なので注意しましょう。
linter でチェックしたうちの、 diff オプションで渡した部分の結果のみを出力します。

参考 : https://github.com/nekonenene/perlcritic_reviewdog

次に github-pr-check reporter ですが、これは見栄えとしてはいいのですが、
README にある通り、これを動作させるためのサーバーは作者の haya14busa さんのポケットマネーで動かしているので、安定した稼働は保証されていません。

また、アプリ連携によりリポジトリのコード閲覧権限を渡すので、
会社で扱う場合には良くない選択肢でしょう。

reviewdog のアプリ連携画面

というわけで、 github-pr-review reporter を使うことになります。

これの使用にあたっては、 https://github.com/settings/tokens から作成できる
GitHub の Personal access token が必要になります。

公開リポジトリなら public_repo のみにチェック、
非公開リポジトリなら大項目の repo の方にチェックを入れて Token を作成します。

f:id:nekonenene:20190713220213p:plain
公開リポジトリなら public_repo のみにチェック

reviewdog のコメントは、この Personal access token の持ち主からおこなわれる形になるので、
reviewdog 用のアカウントがあると見栄えは良いかもしれません。

reviewdog 用のアカウントを作らないと、自分のプルリクにコメントしまくってる人みたいに…

4-2. 必要な環境変数

-reporter=github-pr-review の使用にあたってはたくさんの環境変数が必要になります。

  • REVIEWDOG_GITHUB_API_TOKEN : 上記 4-1. で説明した Personal access token
  • CI_REPO_OWNER : リポジトリの管理ユーザー名(GitHub ID)
  • CI_REPO_NAME : リポジトリ名
  • CI_PULL_REQUEST : プルリクエスト番号
  • CI_COMMIT : 対象ブランチの最新コミットID( git rev-parse HEAD で取得可能)

サポートされているCI(2019/07/13 現在は Travis CI, Circle CI, GitLabCI)であれば Personal access token だけ渡してあげればいいのですが、
ローカルからの実行や Jenkins からの実行に関しては、これらを全て埋めてあげる必要があることに注意しましょう。

4-3. -efm=%f:%l:%c:%m

efm オプションは大事です。が、
perlcritic の場合は出力形式をいじれるので、どちらかと言えば perlcritic 側の出力形式をいじることが大事になるでしょう。
verbose オプションですね。

%f:%l:%c については、reviewdog がどこにコメントすればいいかを示します。
%m はコメントの内容ですので、ここが工夫のしどころです!

最終的には以下のようになりました。

perlcritic --profile .perlcriticrc --verbose '%f:%l:%c:**%m**, near <code>%r</code>.<br>(Ref: [%p](https://metacpan.org/pod/Perl::Critic::Policy::%p))\n' | reviewdog -efm=%f:%l:%c:%m -name=perlcritic -reporter=github-pr-review

Markdown を使いつつ、バッククォート ` はシェル側で解釈して処理されてしまうので、
HTML タグを使うことで避けています。

指摘内容のリファレンスに簡単に飛べるようにしたのは良い工夫だったと思います。

5. Jenkins で使う際の工夫

上の 4-2. で環境変数をたくさん埋めなければいけないと書きましたが、
Jenkins の場合はどうすればいいのかについて触れておきましょう。

5-1. REVIEWDOG_GITHUB_API_TOKEN

秘匿情報であるトークンをどう扱うか、
ここが頭を悩ませるところだと思います。

Jenkins の Credentials (認証情報)を使います。

<your_jenkins_host>/credentials/store/system/domain/_/newCredentials の URL から、
『Secret text』として API Token を追加します。

f:id:nekonenene:20190713223618p:plain
認証情報の追加

Jenkinsfile からは以下のように扱います。

withCredentials([string(credentialsId: "reviewdog", variable: "githubToken")]) {
  env.REVIEWDOG_GITHUB_API_TOKEN = githubToken
  def reviewdogResult = sh(
    // ※ブログ公開用にコマンドは実際と変えてあります
    script: "perlcritic --profile .perlcriticrc --verbose '%f:%l:%c:**%m**\n' | reviewdog -efm=%f:%l:%c:%m -name=perlcritic -reporter=github-pr-review",
    returnStdout: true
  ).trim()
  env.REVIEWDOG_GITHUB_API_TOKEN = "" // token が環境変数として残るのを避けるため、空文字を再代入(unset は効かない)
  echo "${reviewdogResult}"
}

withCredentials スコープ外では値がマスクされないため、
githubToken を代入した env.REVIEWDOG_GITHUB_API_TOKEN の値を
env.REVIEWDOG_GITHUB_API_TOKEN = "" で初期化しているのがポイントです。

f:id:nekonenene:20190713224534p:plain
Credentials の値がマスクされている様子

これをやらないと、この後ろで sh "env" なんかがあると、トークンの値が平文で表示されてしまいます。

5-2. CI_REPO_OWNER, CI_REPO_NAME

リポジトリオーナー、リポジトリ名が変わることはめったにないので直接書いても良かったのですが、
汎用性を持たせるため Jenkins の環境変数である JOB_NAME を活用しました。

def jobNameArr = env.JOB_NAME.split("/")
env.CI_REPO_OWNER = jobNameArr[0]
env.CI_REPO_NAME = jobNameArr[1]

5-3. CI_PULL_REQUEST

Jenkins の環境変数 CHANGE_ID がプルリクエストの ID ですので、それをそのまま使用しました。

env.CI_PULL_REQUEST = env.CHANGE_ID

なお、プルリクエストに紐付かない Jenkins ビルドでは env.CHANGE_ID の値は null になっています。

5-4. CI_COMMIT

以下のように取得しました。

env.CI_COMMIT = sh(returnStdout: true, script: "git rev-parse origin/${env.BRANCH_NAME}").trim()

単純に git rev-parse HEAD となっていないのには理由があります。

私達は Jenkins のビルド設定に関し、安全のために
『Discover pull requests from origin』の設定で
『Merging the pull request with the current target branch revision』を選んでいます。

f:id:nekonenene:20190713230653p:plain
Merging the pull request with the current target branch revision

これにより CI は、 checkout scm 内での作業として、
指定ブランチのリポジトリをクローンした直後、
プルリクの向き先(たいていは master)ブランチをマージします。

そのため、HEAD のコミットIDが、プルリク上の最新コミットIDと一致しなくなってしまうのです。
これを、 origin/${env.BRANCH_NAME} のコミットIDを見に行くことにより避けています。

プルリクの最新コミットIDと env.CI_COMMIT の値が一致しない場合、
422 Unprocessable Entity エラーが返ってきてしまうので注意しましょう。

ここはハマりポイントだと思います。

6. そして開発はさらなる高みへ

こうして reviewdog の導入を果たした私達は、
「自分の通った道をよりきれいに」のボーイスカウト精神を意識しながら、
より一貫性のあるコードへ既存コードを整えていくのでした。

最終的に perlcritic の severity を夏さんが 1 (一番厳しいやつ)にしたのは驚きました(笑)

整えていくぞ〜〜!!

というわけで、Perl を使いつつも、誰もが読みやすいコードを目指して日々精進している私達に加わりたい方、
大募集中です!! 一緒に輝きの向こう側を目指しましょう!

一部 Go 言語への移行を少しずつ進めている箇所もあるので、
Go 言語やアーキテクチャーについての知見をお持ちの方も大歓迎です!

ご応募はこちらから〜

www.wantedly.com

まだまだサーバーサイドのメンバーは少ないですので、
「設計も改善も任せろ〜〜!」という方、ぜひぜひご応募ください!!

*1:プラグインによりますが、たいていのプラグインは inline mode をオプションとしてサポートしています

日本最大級のプロダクトマネージャーコミュニティのオフ会#16 に潜入レポ

はじめまして。 ミラティブの坂本としふみです。

先日、日本最大級のプロダクトマネージャーコミュニティである pmjp のオフ会に行ってきました。 今回のブログではそのレポをお送りします。

pmjpとは

いきなり 公式サイト からの引用なのですが、pmjpとは下記のようなコミュニティです。

Product Managers Japan (PMJP)は、主にWeb業界のプロダクトマネジメント・オーナーシップに興味を持つ人々が集まるコミュニティです。現職のプロダクトマネジャーはもとより、エンジニア・デザイナー・プランナーなどなど、様々なバックグラウンドを持つ人々が集っています。

プロダクトマネージャーという言葉が定着するはるか前である、2015年ごろから始まっているコミュニティです。いまでこそプロダクトマネージャーのコミュニティはいくつか存在しますが、その先駆けとなったコミュニティです。年に数回オフ会が開かれており、プロダクトマネージャーやプロダクトマネージャーに興味がある人があつまり、それぞれのノウハウや悩みをシェアしています。

今日のブログは、2019/06/18(火)に行われたオフ会#16のレポートです。

オフ会レポ

オフ会の概要

今回のオフ会会場は、株式会社ジーニーさんでした。新宿のきれいなオフィスで、到着したらまず夜景が綺麗でした。まずは主催者である ninjinkun の挨拶から始まります。

f:id:sakamoto10423:20190624234942p:plain
会場のジーニー社

その後、発表が2つ、LTが3つ、そして懇親会と続きます。

発表

※資料が公開され次第、資料を追記していきます。2019/06/25時点で公開されている発表は資料へのリンクを貼っております。

「よいプロダクトをつくるためのよいチームのつくられかた」

ヌーラボ小久保さん(@yusuke_kokubo)の発表です。

タックマンモデルを使用しながら、良いチームへの変遷をナマナマしく語っていただきました。 ヌーラボ社の主力製品の一つであるBacklogに、新しい機能を実装したチームを実例にして、 得られた教訓を共有頂けました。

小久保さんより資料が公開されているので詳しくは資料を御覧ください。

speakerdeck.com

会場からの質問

トピックがチーム作りということもあり、会場からはチーム作りに関する質問がでました。

  • 1on1や振り返りで本音出せますか?個人的には、飲み会とかに頼ってしまいがちだが、1on1において工夫していることはありますか?
    • 現在は本音が出せるようになっている。秘訣は、いきなりオープンに話そうぜ!という雰囲気ではなく、まずは自分自身をオープンにすることが重要。プロダクトの熱意や個人的な事もまずは自分からオープンにしていく。
  • チームがリモートだと思うが、そこにテクニックはあるのか?
    • 本プロジェクトは全員福岡に固まっていたので該当しなかった。リモートはリモートならではチーム作りはあるとおもう。

LeSSのすすめ

Repro株式会社執行役員 林さん

f:id:sakamoto10423:20190625002512p:plain
Repro株式会社執行役員 林さん
次は、Reproの林さんより、LeSSを導入した経緯とその効果についての発表がありました。 LeSSというのは、ファイルの中身を見るコマンド.... ではなく、Large-Scale Scrumの略称です。

Reproの開発にLarge-Scale Scrumを導入したらうまくいったという話をしていただきました。 (※Repro: アプリやWEBに簡単に導入できる分析・マーケティングツール)

資料が公開されていないので、発表の内容を完結にまとめておきます。

Reproはご存知の方も多いプロダクトだと思いますが、非常に多機能で大きなプロダクトとなってきています。2018年にはアプリではなくWEBにも対応したことで、より巨大なプロダクトになってきています。そのような背景もあり、

  • 開発速度の低下
  • PMやQAがプロセス上のボトルネックになった
  • 知識が属人化した

ことが起こっていました。そこで、開発プロセスを見直し、LeSSを導入したら(最初は大変だったが)概ねうまくいったというお話でした。

ということで、林さんの言葉もお借りしながら、LeSSについてまとめておきます。 LeSSは https://less.works/ にそのプロセスがまとめられています。

私が感じたLeSSの特徴をひとつかいておきます。(全部書くとそれだけでブログがおわってしまう...) それは、LeSSは、コンポーネントチームではなく、フィーチャーチームで構成され、バックログは全チームで共通ということです。

f:id:sakamoto10423:20190625003921p:plain
Lessのフィーチャーチームのイメージ

https://less.works/less/structure/feature-teams.html から引用

上記の右のようなチームがフィーチャーチームです。例えば、フリマアプリであれば、出品チーム・落札チームみたいな分け方だったり、サーバーサイド・クライアントみたいな分け方だったりをせず、チーム1,2,3.... のような分け方で、それぞれのチームがそのチームのみで価値の出荷が可能なチームの作り方です。

たしかにこのような仕組みにしておくと、大きなプロダクトでは非常に理になかっていると思いました。コンポーネントチーム制(左)だと、チームの負荷を均一にするには、バックログもコンポーネントA,B,C均等にしておく必要があります。大規模なプロダクトになればなるほど、そんな理想的な状態は実現が難しいだろうなぁと思いつつ聞いていていました。(大規模なプロダクトで左を採用すると、リファクタリングまできれいにできているコンポーネントとそうでないコンポーネントができたりしますね...経験談)

Reproさんと状況が似ている方、是非試してみてはいかがでしょうか?ミラティブ社でも、エンジニアが増えてきたら(末尾で募集してますよ!)、チーム開発の体制やプロセスについて大きく考える時期が来るかもしれません。その日に向けて良いインプットとなりました。

LT

「PM部立ち上げから見る、組織化のメリットと育成論」

f:id:sakamoto10423:20190625010116p:plain
株式会社ジーニー 大橋さん
株式会社ジーニー 大橋弘崇さん

大橋さんからは、先程のReproさんの事例とは逆の状況で、「プロダクトの数が増えてきたのでPM部を作った」というLTをいただきました。

2019年4月にプロダクトマネジメント部を発足させ、現在は10名のプロダクトマネージャーが在籍しているとのことです。部として気をつけて運営していることとしては

  • PMとしての基礎レベルの向上
  • 知見の共有
  • 目線を上げる取り組み

を意識してやれらているとのことでした。

チーム化した結果、メリットは様々あるが目線を上げる取り組みが組織的にできたことが非常に良かったと言われていたのが印象的でした。また、プロダクトマネージャーのスキルはあとから付けれるものが多いが、プロダクトに向き合っていくスタンス自体はあとから身につくものではない。そこのスタンスが高いメンバーをPMチームに入れるというお話をされていました。

「サークルスクエア 〜資金を調達せず、プロダクトを育て続けた18年の記録」

いとうまさし(@itmsc)さん すみません、お話を聞くのに夢中で写真をとり忘れてしまい;;🙇‍♀️

www.slideshare.net

※本稿執筆後に資料が公開されていましたので資料をアップしました。

サークルスクエアは、18年の歴史があるグループウェアです。どんな思いをもって続けてきたか、という話をしていただきました。 www.c-sqr.net

その中で印象的だったお話が、プロダクトを100年続けていきたいという話でした。そのために3つの戦略を採用されているということです。かなり具体的な戦略の話だったので、キーワードだけ記載しておきます。

  • ブートストラップ作戦。資金を調達しない
  • ギャラクシー作戦
  • ラストマン・スタンディング作戦

また、新機能がでるとのことで、その告知もされていました🍦

Happy Ice Cream officeとは

ユーザーストーリーと効果的な開発、そのKPIは対話だよって話

株式会社 Fabric Tokyo 渡辺さん

f:id:sakamoto10423:20190625102819p:plain
Fabric Tokyo 渡辺さん

ユーザーストーリーをどう使うかという実践例の話をされていました。ユーザーストーリーは「つくりましょう!」「うまくいった!」という話が多いのですが、渡辺さんの発表はユーザーストーリーを細かく書いたがうまく行かなかったという実践例で、大変勉強になりました。

もう少し具体的にいうと、ユーザーストーリー(やワイヤーフレームを)PMが書きすぎて、チームのクリエイティビティが損なわれてしまった。という実体験から、より抽象度の高い「課題設定」だけをPMが行い、チームと一緒に作るものを考えるというスタイルに変更されたとの話でした。そのスタイルにするとより課題解決がうまく進んだとのことです。

つまり、開発チームとの会話を増やしたことで、課題解決の質が上がった。つまり、開発のKPIは「対話」というまとめでした。

2019/06/26 15:47追記:渡辺さんから資料が公開されましたので掲載いたしました

speakerdeck.com

懇親会

懇親会では、登壇された方とお話したり、さまざまな会社のプロダクトマネージャーと交流することができます。

f:id:sakamoto10423:20190625103746p:plain
懇親会
pmjpのオフ会は16回めということで、食事の量もドリンクの量もちょうどいいです。一人何本アルコールを開けるかというビール係数、アルコールとノンアルの割合を決めるノンアルレシオというものがあり、コミュニティ内でだいたい値のあたりが付いているという話を昔聞いたことがあります。

まとめ

いろんなフェーズの会社の色んな話を聞くことができ、大変勉強になりました。 プロダクトマネジメントの方法やミッションはそれぞれの会社で違うものの、抽象化して学べるところはたくさんあるなぁと改めて感じました。 私がいまミラティブで経験していることもプロダクトマネージャー界隈の微々たる力になればなぁと思いますので、機会があれば登壇しようかなと思っています。(次の次くらいを狙っています笑)

現役のプロダクトマネージャーの方も、プロダクトマネジメントに興味があるという人も大変勉強になる回なので一度参加されてみてはどうでしょうか。

pmjpのコミュニティには下記から参加できます。オフ会の連絡などもこちらで行われています。

Product Managers Japan (PMJP)

ミラティブはさまざまな職種のメンバーを募集しています

というわけで、オフ会(勉強会)のレポをお送りしましたが、ミラティブでは「勉強会補助制度」と「デクレア制度」というのがあります。

勉強会補助制度というのは、その名の通り勉強会の補助制度です。下記のルールで運用されています。

f:id:sakamoto10423:20190625105439p:plain
勉強会補助制度
また、技術書は基本経費で購入できますので、今回のオフ会ででてきたLeSSに関する本なども購入OKです。

また、デクレア制度というのは、自分の働き方をデクレア(宣言)する!というものです。勉強会に行くから早く帰るといったものから、集中的に新しいスキルをつけたいので今週は残業なしにしたいです。といった使い方です。プロ野球選手が、チームが試合で勝ち続けるために、自分のコンディションにあわせて負荷を調整するのに似ています。 デクレアについて詳しくは👇 note.mu

ミラティブではさまざまな職種のメンバーを募集しています。

全職種の応募はこちら👇 www.mirrativ.co.jp

ちなみに、私がPMをしているプロダクト(今後リリース予定)で、iOSエンジニアを募集しています!👇 www.wantedly.com

「ミラティブを他の人に勧めますか?」というNPS調査をプロダクト改善に活かす方法

はじめまして。 分析チームの坂本としふみです。 今日は、「ミラティブを他の人に勧めますか?」というアンケートをどう分析し、プロダクト開発に活かしていくのかということを記事にしてみたいと思います。

NPS(Net Promoter Score)とは?

f:id:sakamoto10423:20190616154241p:plain
NPS調査の例
アプリやWEBサービスを使っているときに、上記のような質問をどこかで見たことあるのではないでしょうか? これは、Net Promoter Score(NPS)と言われる指標を計測するための質問です。

NPSは、ユーザーの利用継続意向を測るための指標で、多くのアプリやWEBサービスで採用されています。 NPSの計算方法は、点数を下記のように分類し、推奨者の割合から批判者の割合を減算したものをNPSとします。たとえば、推奨者が40%・批判者が8%だった場合、NPSは +32 となります。

点数 評価
10 - 9 推奨者(Promoter) ロイヤルティが高い熱心な顧客。自らが継続購入客であるだけでなく、他者へサービスを勧める『推奨』の役割も担う。
8 - 7 中立者(Passive) 満足はしているが、それ程熱狂的ではなく、競合他社になびきやすい。
6 - 0 批判者(Detractor) 劣悪な関係を強いられた不満客。放置しておくと悪評を広める恐れがある。

ちなみに、NPSの業界平均なども公開されていますが、サービスの特性や国によって異なることも知られています。そのため、NPSを他のサービスと比較するというよりは、自社サービスにおけるNPSの上下に着目することが重要です。

ミラティブもKPIの1つとしてNPSを測定しており、月に1回調査をしています。

自由記入欄を分析し、より深い示唆を得る

一般的なNPS調査では、0-10の点数を質問した後、「その点数にしたのはなぜですか?」という自由記入欄を設けています。この自由記入欄を分析すると、「批判者は何で批判なんだろう?」や「推奨者は何で推奨なんだろう?」という突っ込んだ示唆を得ることができます。

そこで、日本語を解析する一般的な手法の一つである、形態素解析を用います。形態素解析とは、文章を形態素(意味をもつ最小単位) に分解し、その形態素の品詞などを解析していく手法です。例えば、 Pythonで書いてみると下記のようなコードになります。

コード

import MeCab as mc
tagger = mc.Tagger('')
results = tagger.parse('ゲーム配信が簡単にできるので')
print(results)

結果

ゲーム  名詞,一般,*,*,*,*,ゲーム,ゲーム,ゲーム
配信    名詞,サ変接続,*,*,*,*,配信,ハイシン,ハイシン
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
簡単    名詞,形容動詞語幹,*,*,*,*,簡単,カンタン,カンタン
に      助詞,副詞化,*,*,*,*,に,ニ,ニ
できる   動詞,自立,*,*,一段,基本形,できる,デキル,デキル
ので    助詞,接続助詞,*,*,*,*,ので,ノデ,ノデ

※上記結果は、Colaboratory上のJupyter ノートブック (python3)で動作確認済

「ゲーム配信が簡単にできるので」という文章が「ゲーム / 配信 / が / 簡単 / に / できる / ので」と分解され、それぞれの品詞が推定されている事がわかります。

ということは、形態素解析を使うと 推奨者に多い言葉な何なのか? その逆に、批判者に多い言葉な何なのか? 推奨者だけによく出てくる言葉はなんなのか? というような解析ができるということがイメージできたんじゃなかなと思います。

では実装だ!となるのもよいのですが、世の中にはきっと同じことを考えているひとがいるはずです。自社で開発する必要のあるものだけに集中し、車輪の再発明を避けるのはベンチャー企業の鉄則です。ということで、NPSの自由記入欄分析に有用なデータ分析サービスを紹介します。

ユーザーローカルによる分析

ミラティブでは、ユーザーローカル というサービスを利用しています。ユーザーローカルを使用すると、2つの文章の比較をすることができます。ワードクラウドとよばれる出現回数を表現する図や、単語分類といった図を作成することができます。

f:id:sakamoto10423:20190616155153p:plain
サイクロン掃除機のレビューを比較した例

たとえば上記の例であれば、「吸い込む」と「吸引力」が高評価・低評価ともに頻度高く出現しています。おそらく、吸い込む力についてユーザーが着目しており、期待を上回って満足する場合とそうでない場合の両方があるのかな?ということが推測できます。また、「サイクロン」という言葉は高評価に偏在しているのは、おそらく「サイクロン掃除機がほしい」というニーズを満たしているからではないかと考えることができます。

ミラティブのNPS調査、自由記入欄を解析してみた

ミラティブのNPS調査の自由記入欄を解析してみたのが下記の結果です。

f:id:sakamoto10423:20190616155310p:plain
ミラティブのNPS形態素解析結果1

f:id:sakamoto10423:20190616155352p:plain
ミラティブのNPS形態素解析結果2

このデータを下記のような観点で眺めています。

カテゴリ 眺める視点 具体的には
両方によく出る 良くも悪くもユーザーが注目している機能や感情 「配信」「エモモ」などのの機能だったり、「楽しい」「面白い」というの感情は推奨者・批判者ともに注目されています。 逆に、「ギフト」「エモカラ」などの新機能の言及が少ないので、より浸透させていく余地があるなと感じています。
推奨者にだけ出現・推奨者によく出る 運営がおいているプロダクトの価値とずれていないか確認 「簡単」「交流」「つながり」などの単語が目立ちます。これは、「わかり合う願いをつなごう」という弊社のビジョンや、「スマホ一台で簡単にゲーム配信」というミラティブのコンセプトが受け入れられているんだろうなと感じています。
批判者にだけ出現・批判者によく出る 今後の改善ポイントとして認識 「バグ(る)」「重い」「ラグ」「不具合」など、品質に関する論点が頻度高く出現しています。配信アプリという特性上、「軽くてラグがなくて不具合がない」という品質が強く求められているんだと感じています。

※上記以外にもたくさん示唆はでるのですが、あまり書きすぎると今後のプロダクト戦略が赤裸々になってしまうのでこのへんでご勘弁を。

ちなみに、批判者に頻出している「バレる」「身バレ」などの単語は、「ミラティブ(バーチャル)上の人格やつながりをリアルな知人に知られたくない」という文脈です。これはNPS質問の「ミラティブを友人・知人に勧めますか?」という質問の特性上、「アプリには満足しているけど、リアルな友人に勧めたくない」という人が批判者に入ってしまうためです。今後は、そういった文脈を排除できるよう、質問の内容を改善中です。

また、今回は形態素の出現頻度のみに着目し、示唆だしを行いましたが、文章解析には、より文脈を数値化するような手法など、様々な手法が存在します。NPS調査に関して言うと、「傾向を早くつかみ、プロダクト開発のPDCAをまわす」ということが重要だと判断し、頻度分析にとどめています。(※この分析手法を選択した結果、ミラティブでは、NPSは実施から分析まですべて、非エンジニアであるコミュニティチームが回すことができています。)

解析結果からのネクストアクション

上記の結果から、やはり不具合解消の優先度を上げるべきだということで、まずはNPS調査で挙げられている不具合にはどのようなものが多いのかをリストアップしました。ちなみに、ミラティブのNPS調査では、回答者ごとに暗号化されたuser_idを自動付与しているので、それを復号することでOSや機種も特定することができます。

f:id:sakamoto10423:20190616155845p:plain
事前に値(user_idのハッシュ)が設定されたアンケート画面
※Googleフォームに事前に値をセットする方法はこちらのヘルプページが詳しいです。

そして、NPSに強く影響する不具合を一覧にしてPMと優先度ミーティングを開催し、バックログに積みました。 また、ラグに関しては、「低遅延な配信を可能にする技術」の研究開発を行っています。(低遅延に関する取り組みはテックブログの過去の記事を御覧ください:Mirrativにおける低遅延配信への取り組みについて【開発中】)

ミラティブではエンジニアを募集しています

このようにミラティブでは、ユーザーの声を聞きながら開発をすすめています。 NPSのKPI化だけではなく、毎週金曜日には、ユーザーからの問い合わせのサマリー・AppStoreやGooglePlayレビューのサマリーをグローバル全社員で共有するなど、職種とわず全員がユーザーと向き合う時間をもっています。

そのあたりの雰囲気は、体験入社をしていただけるともっともっと深まるかなと思いますのでお気軽にご応募ください。

www.mirrativ.co.jp

【WWDC2019】ReplayKitラボで聞いてきたこととAppleへのフィードバック

こんにちは、Mirrativ iOSエンジニアの千吉良(ちぎら)です! 先日サンノゼで開催されたWWDC2019に参加してきました。Mirrativ iOSアプリは ReplayKit を利用して配信を行っていて、ReplayKitラボで質問したい項目を事前にまとめてWWDCに臨みました。

今回はReplayKitを軽く紹介し、ReplayKitラボでしてきた質問とその後Appleへ送ったフィードバックについて、公開できると思われる範囲で一部ご紹介します。

続きを読む

【Android】デバッグツールを変更して開発体験を向上する

Mirrativ Androidエンジニアのmorizoooです。

今回は Mirrativ Androidアプリにおいてデバッグ用のツールをStethoからFlipperに変更して開発体験が向上した事例を紹介します。

Stethoとは

Facebookが開発したネットワーク通信デバッグ用のブリッジライブラリで、
Chrome Developer Toolsを使ってネットワーク通信/Preference/SQLiteの確認ができます。
Mirrativ内では主にネットワーク通信確認で使っていました。
f:id:morizo999:20190603210528p:plain facebook.github.io

Stethoの課題

とても便利なツールではあるのですが、ネットワーク通信を確認しながらアプリを起動していると度々クラッシュが起きていました。
Chrome Developer Toolsでアタッチしていないと発生しないのですが、デバッグ時にストレスがたまるので他の代替案を探していました。
github.com

Flipperとは

こちらもFacebook開発したモバイル用のデバッグツールです。
大きな違いはElectronで書かれた専用のアプリが存在し、AndroidだけではなくiOSにも対応しています。
fbflipper.com

Flipperの導入方法

Getting Started · Flipper
ドキュメントに従えばOK
v0.21.0からno-opができたので導入が楽になりました。 以下の設定をしています。

private var client: FlipperClient? = null
// アプリケーション初期化時に呼ぶ
fun setup(context: Context) {
    SoLoader.init(context, false)

    if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(context)) {
        client = AndroidFlipperClient.getInstance(context).apply {
            addPlugin(DatabasesFlipperPlugin(context))
            addPlugin(FrescoFlipperPlugin())
            addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
            addPlugin(LeakCanaryFlipperPlugin())
            addPlugin(NavigationFlipperPlugin.getInstance())
            addPlugin(NetworkFlipperPlugin())
            addPlugin(SharedPreferencesFlipperPlugin(context))
        }
        client?.start()
    }
}

// OkHttpClient作成時に呼ぶ
fun addInterceptor(builder: OkHttpClient.Builder) = builder.also {
    client?.let {
        builder.addInterceptor(FlipperOkhttpInterceptor(it.getPluginByClass(NetworkFlipperPlugin::class.java)))
    }
}

Flipperの所感

とても良いところ

  • Chrome Developer Toolsでアタッチする必要がない
    (Stethoだとインストール時に毎回選択する必要があるしクラッシュすると通信情報が見えなくなる)
  • レスポンスのHeader/Request/Responseが一体となっていてわかりやすい f:id:morizo999:20190604172911p:plain

  • Flipperアプリ単体でadbのログがみれる

良いところ

  • Fresco(Facebookが開発している画像ライブラリ)だと画像が見やすい&画像のキャッシュ状況がわかる f:id:morizo999:20190604173015p:plain

  • Preferenceの内容と変更履歴が見れる&編集可能(Stethoでも内容確認と編集はできる) f:id:morizo999:20190605130720p:plain

  • Flipperアプリ単体でスクショ/動画が取れる

マイナス

* SQLiteの内容が見えない(そのうちできそう
Feature Request Adding Support for SqlLite DB browser and Shared Preference · Issue #33 · facebook/flipper · GitHub

v0.21.0で入った

喜びの声

f:id:morizo999:20190604155300p:plain

オマケ

iOSでも以下の画像のようにライブラリを導入するだけでネットワーク通信, Layout Inspectorが見えます。
UserDefaultsの内容と変更履歴が見れる&編集も可能 (動作するのはシミュレータだけ

  • ネットワーク通信 f:id:morizo999:20190604160544p:plain

  • Layout Inspector f:id:morizo999:20190604155948p:plain

まとめ

Flipperとても便利なので使っていきましょう!!!

We are hiring!
一緒に開発してくれるエンジニアを募集中です! 体験入社というのもあるのでお気軽にどうぞ!

www.mirrativ.co.jp

MirrativのAPI通信に関するiOS実装の改善とCodableの活用

こんにちは Mirrativ iOSエンジニアの千吉良です。

今回は Mirrativ iOSアプリにおいてAPI通信に関する実装の改善を行ない、関連してCodableを活用した話をしたいと思います。

Mirrativ iOSアプリのソースコードには歴史があり、API通信に関するコアの実装は約4年前に実装されたものでした。日々実装を行なっていていくつか課題を感じていた箇所がありましたが、API通信に関する実装はコード量も多く、新しい仕組みを導入して一気に刷新しようというわけにはいきません。既存のファイル構成などを活かしつつ軽量な仕組みを導入して、新規実装のみ新しい方法で実装したり、部分的に修正していけるような実装を考える必要がありました。

続きを読む

Mirrativにおけるプッシュ通知ぼかしへの挑戦

こんにちは Mirrativ CTOの夏です

今回は先日iOSでリリースした通知ぼかし機能について、裏でどういうことをしているのかについて軽く紹介したいと思います。

f:id:hottestseason:20190517204251p:plain

MirrativではOSから提供されるAPI( ReplayKitMediaProjection )を用いて、スマホ画面を直接収録・配信することで、配信用のSDKを各ゲームやアプリに埋め込んでもらう必要がなく、ユーザさんが色々なアプリを気軽に切り替えながら友達と雑談を楽しんだり、ゲームを通じて新しい友達を作ることができます。

その反面、配信者が適切な設定を行っていない場合、意図しないプッシュ通知が視聴者に表示されてしまう場合もあります。

Mirrativでは配信準備画面に注意文言と啓蒙動画を載せていますが、どうしても設定が漏れたりする場合があるため、配信者が注意しなくても、プッシュ通知が視聴者に見えない仕組みを以前から検討していました。

f:id:hottestseason:20190517174205:plain:w300

Android側は NotificationListenerService を利用することで、通知を受信したタイミングで配信を一時的に停止する設定を入れています。

f:id:hottestseason:20190517171341p:plain:w300

iOSに関しては他の配信アプリなども参考に、画面に表示される通知を自動で検知し、通知領域にのみモザイクをかける方針を検討し、 副業の方と整理し、以下のような要件で実装してもらうことになりました。 (本人からの希望で名前はお出ししていませんが、以下の実装詳細の説明などに関しても本人にまとめて頂いたものを流用しています)

  • 配信中に配信者の端末に届いたプッシュ通知 (Remote/Local問わず) にモザイクをかけて配信したい
  • ただしMirrativアプリの通知はぼかさなくて良い
    • 配信へのコメントがぼかされてしまうと、視聴者が「配信者がコメントを読めない」という誤解を招く場合がある
  • 対象は画面上部からスライドインしてくるバナー通知のみ。ひとまず通知センターに表示される通知等はモザイクをかけないで良い
    • 能動的なアクションが必要となる通知は、配信者の注意によって防止できる
  • 同様の理由で、配信者が通知をタップした場合等はモザイクが外れてしまっても良い

つまりこいつです。

f:id:hottestseason:20190510154316p:plain
iOS プッシュ通知

副業の方に実装をお願いする以前、iOS11から利用できるVisionフレームワークの短形検知を利用したプロトタイプを試していたのですが、ゲームなどが動いているバックグラウンドで、OSに一番殺されやすいプロセスの中、さらに配信しながら行う処理であることを考えると、CPU使用率やメモリ使用量などが懸念点となり、汎用的な手法を取るより、プッシュ通知という特定の領域に絞って自前で検知アルゴリズムを実装することになりました。

基本方針の検討

iOSには現状、他アプリからのプッシュ通知イベントをフックする手段が無いため、配信用にサンプルされた画像から通知の存在有無を判定し、その領域を検出する画像処理が必要となります。 通知領域が検出できれば、あとは単純にその領域に対してモザイクをかければよいでしょう。

考えうる方針としては以下があると思われます。

  1. 機械学習的なアプローチ
  2. 非・機械学習的なアプローチ
    1. 既存の汎用的なアルゴリズム
    2. 専用特化したアルゴリズム

1.の機械学習的なアプローチは、背景画像や端末解像度の違い等に対して頑健に作れる、細かな閾値調整等が (2-bと比較して) あまり必要ない、等のメリットはあるものの、一方で学習のために一定量の教師データ (特に正例) が必要となる、デバッグが困難である等のデメリットがあります。

2-a. 既存アルゴリズムは、例えば古典的なCanny法やHough変換などを用いることは可能であり、うまくすれば将来的なUIの変更等に対して非常に頑健に作れる可能性があるものの、基本的に計算量が大きくなるというデメリットがあります。通知ぼかしはゲームアプリ等のプレイ中にも常時動かす処理であるため、計算量は限界まで削減したい所です。

また、これら既存アルゴリズムの対象が主に風景などの自然写真であるのに対して、今回の対象は固定的な通知領域UIであるため、その削減余地は非常に大きいと言えます(エッジの角度を一切気にしないで良い、出現位置を事前に調べることが可能、etc)。

以上から、今回は通知領域検出に特化したルールベースのアプローチを取ります。整理すると、メリットとデメリットは以下となります。

  • メリット
    • 教師データの収集が必要ない (vs 1)
    • 実行時パフォーマンスが高い (vs 2-a)
  • デメリット
    • 閾値等を手動で微調整する必要がある
      • OSアップデートによって通知領域の形状が変更されるなどした場合、閾値を再調整する必要が出てくるかもしれない
    • 端末解像度の違いなどに対して感度が高い
      • 新しい解像度の端末が発売されるために、データを追加する必要がある
      • ただしこの運用コストを多少緩和するアプリを作った。詳細は後述

今回は、メリットに上げた点を優先するためにこれらのデメリットを犠牲にしました。なお、この実装によって正例を自動収集することが可能になるので、それを用いて将来的に機械学習的アプローチに乗り換えていくという方法も考えられますが、収集の仕組みの実装・機能再実装・再QA等のコスト、そして結局UI変更に対しては脆弱であり運用コストをゼロには出来ない点等を考えると、(微妙な所ですが) 現行アプローチの継続に軍配が上がると思われます。

通知領域の検出

通知領域の定義

検出したい通知領域は、例えば以下のような角丸矩形ですが、

f:id:hottestseason:20190510154316p:plain

プッシュ通知は画面上部からスライドインしてくるため、上のような完全な角丸矩形でしか検出できない場合、スライドイン中の通知内容が見えてしまうことになります。よって、以下のような角丸矩形の下部を検出し、その領域を画面最上端まで拡張した領域を検出することにします。

f:id:hottestseason:20190510155649p:plain

ここで言う「領域」(Filled Rectangle) とは、以下2点によって定義できるでしょう。

  1. 外縁がエッジで囲まれている
  2. 内部が塗りつぶされている

これらは機械処理しやすいよう、以下のように換言できます。

  1. 外縁に大きな輝度変化が存在する
  2. 内部に大きな輝度変化が存在しない

f:id:hottestseason:20190510155854p:plain

外縁をまたぐ赤矢印の部分に大きな輝度変化が存在し、内部の青矢印の部分に存在しなければ、領域とみなすことができそうです。矢印の間隔を小さくしていくことで精度は向上しますが、パフォーマンスの関係上、適度に間引くことにします。また、内外どちらも背景色の影響を受ける (特に内部は透かし/ブラーがある) ため、適当な閾値を探す必要があります。

輝度変化の指標

輝度変化を捉える手法はいくつかありますが、ここでは一次微分の分散を用いることにします。具体的には、まず下図中の黄点のように矢印線上のいくつかの点をサンプリングします。

f:id:hottestseason:20190510155954p:plain

その上でRGBそれぞれの一次微分(つまり画素値の差分)を計算し、それらの分散(dv)を下式によって計算します。

image

ここでpiはi番目の画素値、Dは画素間の距離です。dvをRGBそれぞれについて計算し、最大のものを特徴量として使用します。最初にグレースケール化してから計算すると特定ケースにおいて精度が落ちたので、今回はRGBごとに計算するようにしています。

このdv値を、事前に調べておいた閾値と比較し、大きければエッジがあり、小さければエッジが無いと判定することが可能となります。閾値は、外縁・内部ともに2.0が一番精度が高そうでしたが、ここは調整の余地がありますし、将来UIに変更が入った場合も調整する必要が出てくる可能性があります。

検出ロジック

上記のアルゴリズムによって、矢印の両端点が与えられた場合に、それらを繋ぐ直線状にエッジが存在するか否かを判定することが可能となりました。これを用いて、与えられた画像の全領域内に通知領域が存在するか否かを判定していきます。基本的な流れとしては、以下になります。

  1. 矢印の端点群を生成する
  2. 端点群を順に走査し、それぞれエッジ存在判定をする
    • 存在する場合はスコアを加算する
    • bridge(外縁)の場合、スコアは下線部・横線部・角丸部ごとに異なった値を用いる (角丸は通知領域を特徴づける有力な特性であるため強めに評価する。また横線部はそもそもの端点が少ないためこちらも強めに評価する)
    • inner(内部)の場合、スコアは常に1で良い
  3. それぞれのスコアの平均値が基準を満たしているかを、閾値によって判定する
    • 直感的な解釈としては、innerについてはエッジが存在すると判定された矢印の個数の割合。bridgeについてはそれ重み付けを行ったもの
    • つまり、bridgeのスコア平均値が閾値を上回っており、かつinnerのそれが閾値を下回っている場合に、通知領域が存在すると判定する
    • 色々試した結果、bridgeは0.8, innerは0.2で今の所よく検出できている模様

ノックアウト

ある1つの矢印のdv値が既定の閾値を下回った/上回った場合、即座に通知領域が存在しないと判定しbail outします。それぞれ1.0と10.0が効果的でした。これによって、例えば真っ白な画面を判定する際などに、全ての端点ペアを走査することなく即座に検出処理を中止することが可能となり、多少パフォーマンスと検出精度が向上します。

端末ごとの事前情報の調査

上述のアルゴリズムを用いる際、各端末の解像度ごとに通知領域の出現位置を事前に調べておくことが、精度やパフォーマンスの観点から非常に重要となります。例えばiPhone7とiPad miniでは通知領域のx座標が異なりますが、これを事前情報として持っておかないと一定範囲のxに対して検出メソッドの呼び出しをループしなければならなくなり、計算量が飛躍的に増大してしまいます。逆に端末ごとに固定値を調べておくことができれば、x座標を決め打ちすることが可能となります。

この事前情報を調査するため、単にローカル通知を表示するためだけのアプリを実装し、各シミュレータで動かして、通知が表示されたスクショを撮影し、それらに対して非常に単純なロジックによって通知領域を検出して、以下のような端末解像度毎の出現位置を割り出しました。

{
  "1080x1920": {
    "width": 1080,
    "height": 1920,
    "scale": 2.61,
    "notification_region": {
      "x": 21,
      "y": 21,
      "width": 1038,
      "height": 325
    }
  },
  "1125x2436": {
    "width": 1125,
    "height": 2436,
    "scale": 3,
    "notification_region": {
      "x": 24,
      "y": 120,
      "width": 1077,
      "height": 374
    }
  },
  // ...
}

時系列を加味した最適化

上述した手法を用いれば一応通知領域は検出できますが、まだパフォーマンスに改善余地があります。通知領域が存在する可能性のある全領域を30fpsで検索していると流石に重く、手元のiPhone7ではCPU使用率が30-50%程度となりました。よって、どうにかして探索範囲を狭める必要があります。また、対象範囲を狭めることができれば結果的に誤検知が減り精度の向上も期待できます。

ここでは、通知領域の位置の時間的変位が利用できます。噛み砕いて言うと、以下のような想定を置くことが可能です。

  1. 前フレームにおいて、通知領域が検出されなかった場合、探索領域は最上端の一定部分のみでよい (必ず上からスライドインしてくるため)
  2. 前フレームにおいて、ある位置に通知領域が検出された場合、その周辺を優先して探索すればよい

余談ですが、通知領域のスライドアニメーションは一般的なease-in/outなので、これを利用してシグモイド関数等を使って線形モデルよりも精度の高い出現位置予測を出来そうですが、試してみたところそこまで精度が上がらなかったのでやめました。

ただし、前回の検出位置という状態変数を持つことにより、いくつか問題が出てきます。具体的には以下のようなものをケアする必要があります。

  • 通知表示中に配信開始した場合、全領域を探索しないと正しく検出できない
  • 端末回転中は通知検出ができないので、その間に通知が来た場合、全領域を探索しないと正しく検出できない
  • 何かのミスで一瞬通知領域が無いと判定されてしまった場合、次回以降の一定期間も全探索したい (fail-safe的に)

Mirrativからの通知だけモザイクをかけない

通知領域を検出した場合、その通知領域内のアイコン部分を調べ、もしMirrativのアプリアイコンと一致していれば通知領域なしと判定します。 こちらに関しては、SADを用いた単純なテンプレートマッチングを行っています。アルゴリズム詳細はこのへんのサイトを参照して下さい。 Mirrativのアプリアイコンと一致していなければ、対応する領域にCore ImageのCIFilterのうちCIPixellateを適用してモザイクをかけています。 (当初はCIGaussianBlurで実装していたのですが、配信が異常に落ちる事件があった結果、CIPixellateに変更しました)

課題と更なる挑戦

すでにiOSの全ユーザに公開済みの機能とはいえ、まだ特定の端末(新型iPad Proなど)に対応していなかったり、特定のタイミングでMirrativの通知がぼかされたり、 Mirrativ以外の通知がぼかされていなかったりと一部不具合も残っているため、現在CSチームと協力して不具合が起きている箇所の動画を収集しており、さらなる改善に繋げようとしております。

適用領域がニッチとは言え、他にはあまり類をみないソフトウェアだと思うので、さらなる改善後にはOSS化してみようと思っていますので、乞うご期待を!!

ミラティブは多種多様な技術(ライブストリーミング・Unityによる3Dアバター・機械学習などなど)を扱っており、我こそはという方がいらっしゃれば副業などでも構わないので、ぜひご応募ください!!

www.mirrativ.co.jp

追伸

f:id:hottestseason:20190517202752p:plain

追伸の追伸

今年度ミラティブ初めての新卒エンジニアの安西くんがブログを始めました!!

mirrativ-stream.hatenablog.com