macOS アプリ (Cocoa) に GStreamer を入れる手順

GStreamer 公式のドキュメントには Cocoa application に GStreamer を組み込む手順は無いんですが、やってみたら思いのほか簡単だった (Android や iOS に似てた) ので紹介します。

ここでは GStreamer の解説をするつもりは全くなく、「Cocoa application にはこんな感じでGStreamer 入れるんだー、へー。」くらいに見てください。

# 完成形

題材として、 [iOS tutorial 3](tutorial) の macOS 版 + α を作っていきます。

見た目はこんな感じ。

![スクリーンショット 2017-09-29 10.26.13.png](https://qiita-image-store.s3.amazonaws.com/0/97187/2a31cb06-acb7-d97f-4c57-dd39b9d9160d.png)

完成形のソースコードはこちら↓から

https://github.com/tetkuz/Simple-video-viewer

# 環境

- Macbook Pro Early 2015
- macOS Sierra 10.12.6
- Xcode 8.3.3

# 1. GStreamer.framework のインストール

https://gstreamer.freedesktop.org/data/pkg/osx/ からパッケージを2つダウンロードします。
※ 偶数バージョン (`1.10.*`, `1.12.*` とか) のうち新しいものをダウンロードしておけばOK

今回ダウンロードしたもの↓
- `gstreamer-1.0-1.12.3-x86_64.pkg`
- `gstreamer-1.0-devel-1.12.3-x86_64.pkg`

ダウンロードしたら2つともインストールしてください。

# 2. プロジェクトの作成

Cocoa Application として作ります。

![スクリーンショット 2017-09-28 14.27.50.png](https://qiita-image-store.s3.amazonaws.com/0/97187/02ba8793-18bd-d519-6b98-051c25ce47ae.png)

# 3. プロジェクトの設定

プロジェクトの初期設定は [Installing on Mac OS X](install) を参考にやっていきます。

## 3.1. GStreamer.framework の設定

はじめに、Linked Frameworks and Libraries へ `/Library/Frameworks/GStreamer.framework` を追加します.

![スクリーンショット 2017-09-28 14.48.04.png](https://qiita-image-store.s3.amazonaws.com/0/97187/c902830e-87b7-b7eb-ed8d-5ac15989d216.png)

いれるとこんな感じに。

![スクリーンショット 2017-09-28 14.48.11.png](https://qiita-image-store.s3.amazonaws.com/0/97187/c276ce67-823c-fe66-df1e-c6062efe7aa0.png)

## 3.2. Build Settings の設定

Build Settings へ移動し、検索窓に `header search path` と入れます。
その中の `Header Search Paths` に `/Library/Frameworks/GStreamer.framework/Headers` を *recursive* で追加します。

![スクリーンショット 2017-09-28 14.51.33.png](https://qiita-image-store.s3.amazonaws.com/0/97187/4e93a124-59b1-852e-f83e-fc66dd555a2a.png)

次に、検索窓に `c flags` と入れて `Other C Flags` に 
`-I/Library/Frameworks/GStreamer.framework/Headers` を追加します。

![スクリーンショット 2017-09-28 14.52.34.png](https://qiita-image-store.s3.amazonaws.com/0/97187/1826c7ca-1aef-4636-4244-870898969f65.png)

# 4. GStreamer のリンク確認

iOS tutorial 3 相当の実装を始める前に、GStreamer のリンクを確認するため、ミニマムな Tutorial 1 (画面なし) の実装をしてみます。

## 4.1. GStreamerBackend クラスの追加

`Cocoa class` を選択して追加します。

![スクリーンショット 2017-09-28 14.58.26.png](https://qiita-image-store.s3.amazonaws.com/0/97187/207d4ff5-ab8d-4ce5-1075-825592d8a22b.png)

言語は `Objective-C` を選択します (間違って swift を選択した場合はやり直しです :boom: )

![スクリーンショット 2017-09-28 14.58.21.png](https://qiita-image-store.s3.amazonaws.com/0/97187/d2119c73-0633-7fff-3c8c-c2092e4eb306.png)

「Bridging header いる?」って聞かれるので作っておいてください。

![スクリーンショット 2017-09-28 14.56.12.png](https://qiita-image-store.s3.amazonaws.com/0/97187/ab386639-2a7c-aac9-1758-5dc773b0eedf.png)

## 4.2. GStreamerBackend クラスの実装

クラス追加ができたらあとは実装です。

```objc:GStreamerBackend.h
#import <Foundation/Foundation.h>

@interface GStreamerBackend : NSObject

-(NSString*) getGStreamerVersion;

@end
```

```objc:GStreamerBackend.m
#import "GStreamerBackend.h"

#include <gst/gst.h>

@implementation GStreamerBackend

-(NSString*) getGStreamerVersion
{
    char *version_utf8 = gst_version_string();
    NSString *version_string = [NSString stringWithUTF8String:version_utf8];
    g_free(version_utf8);
    return version_string;
}

@end
```

## 4.3. Bridge-Header に GStreamerBackend を追加

Swift から Objective-C のクラスを使用するために Bridge header に `GStreamerBackend.h` を追加します

```objc:SimpleVideoViewer-Bridging-Header.h
#import "GStreamerBackend.h"
```

## 4.4. ViewController から GStreamerBackend クラスを使う

GStreamerBackend インスタンスを作って、`getGStreamerVersion()` を叩きます。

```swift:ViewController.swift
import Cocoa

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        print(GStreamerBackend().getGStreamerVersion())
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }
}
```

## 4.5. 動かしてみる

以上で iOS tutorial 1 相当の実装ができました。実際に動かしてみましょう。

![スクリーンショット 2017-09-28 15.13.17.png](https://qiita-image-store.s3.amazonaws.com/0/97187/078d90de-ddff-4bb4-f007-6211a033edce.png)

デバッグログに GStreamer バージョンが表示されるなんとも味気ないアプリの完成です :tada: 

# 5. 画面作り

ここからやっと iOS tutorial 3 相当の実装が始まります。まずは画面を作りましょう。

ボタンの配置や Auto layout を設定するかは自由ですが、以下の 5つの View と 2つの Constraint は必ず必要です。(その他は気分で設定してください笑)

- Views
  - Button x2 → 再生と一時停止
  - Label x1 → パイプラインの状態を表示するためのメッセージ
  - CustomBox x1 → 動画の表示領域を決定
  - CustomView x1 → GStreamer の描画先
- Constraints
  - CustomView の width
  - CustomView の height

![スクリーンショット 2017-09-28 16.28.33.png](https://qiita-image-store.s3.amazonaws.com/0/97187/942d9900-9e65-649f-77eb-152a9d5bfbc4.png)

ちなみに、 iOS tutorial 3 では UIView 中に UIView だったんですが、Appkit だと StoryBoard 上で NSView の background color を付けれなかったので Box を使っています。

background color を気にしないのであれば、NSView の中に NSView でも全く問題なしです :)

# 6. Storyboard と ViewController の連携

iOS アプリ作っている人にはおなじみの *アレ* です。ペタペタしてやってください。
※ CustomBox に関しては、Box の中にある NSView を Container としてください。

**StoryBoard**

![スクリーンショット 2017-09-28 16.53.37.png](https://qiita-image-store.s3.amazonaws.com/0/97187/362356a8-2c7e-2136-0ef8-5b5e7b9f46f4.png)

**ViewController**

```diff:ViewController.swift
  import Cocoa

  class ViewController: NSViewController, GStreamerBackendDelegate {

+    @IBOutlet weak var video_container_view: NSView!
+    @IBOutlet weak var video_view: NSView!
+    @IBOutlet weak var video_view_width_constraint: NSLayoutConstraint!
+    @IBOutlet weak var video_view_height_constraint: NSLayoutConstraint!

+    @IBOutlet weak var play_button: NSButton!
+    @IBOutlet weak var pause_button: NSButton!
+    @IBOutlet weak var message_label: NSTextField!

      override func viewDidLoad() {
          super.viewDidLoad()
      }

      [省略]
  }
```

# 7. OpenglNSView の実装

GStreamer の描画先として NSView を使うには EaglUIView 相当のもの (= OpenglNSView) を作る必要があります。

> **iOS tutorial 3: EaglUIView** 
> https://gstreamer.freedesktop.org/documentation/tutorials/ios/video.html#eagluiview

`New File...` から Swift file を追加し `OpenglNSView.swift` を作ります。

OpenglNSView.swift に以下のコードをコピペします。

```swift:OpenglNSView.swift
import Foundation
import OpenGL
import Cocoa

class OpenglNSView: NSView {
    override func makeBackingLayer() -> CALayer {
        return CAOpenGLLayer();
    }
}
```

最後に StoryBoard の Video view のクラスを OpenglNSView に変更します。

![スクリーンショット 2017-09-28 16.40.25.png](https://qiita-image-store.s3.amazonaws.com/0/97187/7be1f02c-d045-d9f9-a6b0-3225f93c031b.png)


OpenglNSView の詳細が気になる人は [チュートリアル](https://gstreamer.freedesktop.org/documentation/tutorials/ios/video.html) と [Apple のドキュメント](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/SettingUpLayerObjects/SettingUpLayerObjects.html) を読んでみてください。

# 8. iOS tutorial 3からコードをコピペ

iOS tutorial 3 から以下の3つのコードをコピペしてきます。

- [GStreamerBackendDelegate.h](
https://cgit.freedesktop.org/gstreamer/gst-docs/tree/examples/tutorials/xcode%20iOS/Tutorial%203/GStreamerBackendDelegate.h)
- [GStreamerBackend.h](https://cgit.freedesktop.org/gstreamer/gst-docs/tree/examples/tutorials/xcode%20iOS/Tutorial%203/GStreamerBackend.h)
- [GStreamerBackend.m](https://cgit.freedesktop.org/gstreamer/gst-docs/tree/examples/tutorials/xcode%20iOS/Tutorial%203/GStreamerBackend.m)

# 9. ビルドを通す

チュートリアルからコピペしただけではビルドが通りません。通すために以下の２つを行います。

1. UIView を NSView に置換
2. NSView のために Cocoa/Cocoa.h をインポート

## UIView の置換

UIView を NSView に置換します。Xcode の Replace 機能なり、sed なりで行ってください。全部でこのくらいあるはずです。

![スクリーンショット 2017-09-28 16.45.09.png](https://qiita-image-store.s3.amazonaws.com/0/97187/7294aad7-84ad-36e0-5577-7f461b2d2eda.png)

## Cocoa/Cocoa.h のインポート

GStreamerBackend.h に `Cocoa/Cocoa.h` を追加します。

```diff:GStreamerBackend.h
  #import <Foundation/Foundation.h>
+ #import <Cocoa/Cocoa.h>
  #import "GStreamerBackendDelegate.h"

  @interface GStreamerBackend : NSObject

  [省略]

  @end
```

この2ステップを行えば **ビルドは通るようになった** はずです。

# 10. ViewController の実装

チュートリアルの ViewController を swift 化します。

```swift:ViewController.swift
import Cocoa

class ViewController: NSViewController, GStreamerBackendDelegate {

    @IBOutlet weak var video_container_view: NSView!
    @IBOutlet weak var video_view: NSView!
    @IBOutlet weak var video_view_width_constraint: NSLayoutConstraint!
    @IBOutlet weak var video_view_height_constraint: NSLayoutConstraint!

    @IBOutlet weak var play_button: NSButton!
    @IBOutlet weak var pause_button: NSButton!
    @IBOutlet weak var message_label: NSTextField!

    let media_width: CGFloat = 320
    let media_height: CGFloat = 240
    var gst_backend: GStreamerBackend? = nil

    override func viewDidLoad() {
        super.viewDidLoad()

        play_button.isEnabled = false
        pause_button.isEnabled = false

        gst_backend = GStreamerBackend.init(self, videoView: video_view)
    }

    @IBAction func play(_ sender: Any) {
        gst_backend?.play()
    }

    @IBAction func pause(_ sender: Any) {
        gst_backend?.pause()
    }

    override func viewDidLayout() {
        let view_width: CGFloat = video_container_view.bounds.size.width;
        let view_height: CGFloat = video_container_view.bounds.size.height;
        
        let correct_height = view_width * media_height / media_width;
        let correct_width = view_height * media_width / media_height;
        
        if (correct_height < view_height) {
            video_view_height_constraint.constant = correct_height;
            video_view_width_constraint.constant = view_width;
        } else {
            video_view_width_constraint.constant = correct_width;
            video_view_height_constraint.constant = view_height;
        }
    }

    func gstreamerInitialized() {
        DispatchQueue.main.async {
            self.play_button.isEnabled = true
            self.pause_button.isEnabled = true
            self.message_label.stringValue = "Ready"
        }
    }

    func gstreamerSetUIMessage(_ message: String!) {
        DispatchQueue.main.async {
            self.message_label.stringValue = message
        }
    }
}
```

# 11. gst_init() を追加

ViewController を実装して完成！と思いきや、 **重要なステップ** が残っています。

そう、まだこのコードは gst_init() をしてないんです。gst_init() をしなければ、プラグインのロードも行われないので、「エレメント見つかりません」エラーが出てしまいます。

`GStreamerBackend.m` の `app_function()` へ `gst_init(NULL, NULL)` 追加しましょう。

```objc:GStreamerBackend.m
[省略]

/* Main method for the bus monitoring code */
-(void) app_function
{
    GstBus *bus;
    GSource *bus_source;
    GError *error = NULL;

    gst_init(NULL, NULL);
    
    GST_DEBUG ("Creating pipeline");

    /* Create our own GLib Main Context and make it the default one */
    context = g_main_context_new ();
    g_main_context_push_thread_default(context);

    /* Build pipeline */
    pipeline = gst_parse_launch("playbin uri=https://download.blender.org/durian/trailer/sintel_trailer-480p.mp4", &error);
    if (error) {
        gchar *message = g_strdup_printf("Unable to build pipeline: %s", error->message);
        g_clear_error (&error);
        [self setUIMessage:message];
        g_free (message);
        return;
    }

    [省略]
}

@end
```

↑ここではついでに `gst_parse_launch()` のパイプラインも変えておきました。この辺は好みで変えてください。

# 完成

お疲れ様です、以上で完成です！ :tada:

重要なところは、 `7. OpenglNSView の実装`, `9. ビルドを通す`, `11. gst_init() の追加` くらいなものなので iOS → macOS は意外と簡単ですね。

あとは、パイプラインを変えるなり UI をいじるなりして遊んでみてください :)