[macOS] SafariのようにWKWebView単位でミュートをかける [hack]

Safariのようにタブ単位でミュートをかけたい！と検索していたら見つかったので書き記しておきます。

2018年2月15日更新
High Sierraにて`-[WKWebView _setPageMute:]`メソッドが取り込まれましたので、新たに記事をかきました。
[SafariのようにWKWebView単位でミュートをかける 最新版](https://qiita.com/masakihori/items/4e3efd966f56afc986e0)

**この方法にはWebKitへのハックが含まれており、WebKitのアップデートにより利用不可能となる可能性があります。**

**また、WebKitの次のアップデートでより簡単な方法が使えるようになる可能性があります。**

### プロジェクトファイル一式
文章を読むの面倒な方はこれをみてください。
https://github.com/masakih/WKWebKitMuteExt

# ハックする場所
ここです。
https://github.com/WebKit/webkit/blob/5277f6fb92b0c03958265d24a7692142f7bdeaf8/Source/WebKit2/UIProcess/API/Cocoa/WKWebView.mm#L4426

```objc++
@implementation WKWebView {

...

- (void)_setPageMuted:(_WKMediaMutedState)mutedState
{
    WebCore::MediaProducer::MutedStateFlags coreState = WebCore::MediaProducer::NoneMuted;

    if (mutedState & _WKMediaAudioMuted)
        coreState |= WebCore::MediaProducer::AudioIsMuted;
    if (mutedState & _WKMediaCaptureDevicesMuted)
        coreState |= WebCore::MediaProducer::CaptureDevicesAreMuted;

    _page->setMuted(coreState);
}

...

}
```

https://github.com/WebKit/webkit/blob/master/Source/WebKit2/UIProcess/API/Cocoa/WKWebViewInternal.h#L69

```objc++
@interface WKWebView () WK_WEB_VIEW_PROTOCOLS {

...

    RefPtr<WebKit::WebPageProxy> _page;

...

}
```

https://github.com/WebKit/webkit/blob/39cd65daaeadf2647294751ce302efa224701718/Source/WebKit2/UIProcess/WebPageProxy.cpp#L4181

```cpp
void WebPageProxy::setMuted(WebCore::MediaProducer::MutedStateFlags state)

```

https://github.com/WebKit/webkit/blob/5277f6fb92b0c03958265d24a7692142f7bdeaf8/Source/WebCore/page/MediaProducer.h#L56

```objc
enum MutedState {
        NoneMuted = 0,
        AudioIsMuted = 1 << 0,
        CaptureDevicesAreMuted = 1 << 1,
    };
    typedef unsigned MutedStateFlags;
```



`-[WKWebView _setPageMuted:]`はgithubへは２０１７年４月１３日にマージされています。
この変更はまだmacOSに反映されていません。（2017年5月22日現在）

これと同じことをカテゴリなどで実装するのが今回の趣旨です。


## 方法

簡単に言えば、 `WKWebView`の`_page` プロパティのインスタンス関数 `WebPageProxy::setMuted()` を呼べばいいだけです。

ただ、`_page`がC++のオブジェクトのためちょっと面倒臭いことが必要です。


## Power Tool
twitterで @hetima さんより強力なツールを教えていただきました。　正直何をやっているかよくわかってません。

https://github.com/hetima/HTSymbolHook

機能は関数などのシンボルをオブジェクトファイルなどから検索しアドレスを取得するなどです。
主機能はアドレスを差し替えてライブラリの機能を上書きする、もののようです。

今回はシンボル名からアドレスを取り出すだけに使っています。

## シンボルを調べる
githubに上げているものは実行時に調べていますが通常は

```shell-session
$ nm /path/to/lib | grep [name of class] | grep [name of method]
```
あたりで調べられます。
最後のカラムがシンボル名です。

今回見つかったシンボルは
`__ZN6WebKit12WebPageProxy8setMutedEj`
です
なんとなく`WebKit::WebPageProxy::setMuted` が見えますね。

## シンボルからアドレスを取り出す
上の方法で`WebPageProxy::setMuted()`のシンボルが`__ZN6WebKit12WebPageProxy8setMutedEj`とわかりましたので、これからアドレスを取り出します。

@hetima さんのHTSymbolHookを使います。

```objc++
typedef void (*SetMuteFunc)(void*, NSInteger);

NSString *symName = @"__ZN6WebKit12WebPageProxy8setMutedEj";

SetMuteFunc getSetMuteFunc() {
    HTSymbolHook *hook = [HTSymbolHook symbolHookWithImageNameSuffix:@"/WebKit"];
    return (SetMuteFunc)[hook symbolPtrWithSymbolName:symName];
}
```

これでインスタンス関数のアドレスが取得できます。

## WKWebViewから_pageプロパティを取り出す
これは普通に`objc/runtime.h`の各関数を使います。

```c
static const char *pagePropertyName = "_page";

void *getPage(id instance) {
    Class classObj = object_getClass(instance);
    Ivar ivar = class_getInstanceVariable(classObj, pagePropertyName);
    void *ptr = object_getIvar(instance, ivar);
    
    return ptr;
}
```

注意しておきたいのはARCが有効だとC++のインスタンスに対し `objc_retainAutoreleasedReturnValue()` が使われてしまうためクラッシュしてしまうことです。
別ファイルに分けてARCを無効にしておきましょう。

## ミュートする
必要な値は全て揃いましたので、これらを使ってWKWebViewをミュートします。

```objc

typedef void (*SetMuteFunc)(void*, NSInteger);

WKWebView *webView;

void *page = getPage(webView);
SetMuteFunc setMute = getSetMuteFunc();
setMute(page, AudioIsMuted);

// setMute(page, NoneMuted);
```


# githubにあげたプロジェクトの概要
上にあげたプロジェクトでは `WKWebView`に`mute`プロパティを設けて簡単に設定できるようにしています。
また、`WebPageProxy::setMuted()`のシンボルを実行時に検索するようにし、可能な限りWebKitのアップデートに対応できるようにしています。


## おわりに

C++の中身は全くわかりませんでしたが、 @hetima さんの数々の助言でやりたいことが実現しました。
本当にありがとうございます。

ただ、使おうと思っていたプロジェクトで他の制約からWKWebViewが使えないということが発覚し僕の中では残念な結果で終わってしまいました。

そのまま捨て去るのが癪に触るのでここに記事をあげさせていただきました。
お目汚しを...

あと、上に書いた通り、最近`WKWebView`にミュート用メソッドが追加されていますので、近いうちにmacOSにも入って簡単にハックできるようになるかもしれません。
使いたい人は注視しておくといいかも？