読者です 読者をやめる 読者になる 読者になる

文鎮iPhone救出計画 〜ネットワークカメラ化〜

HaishinKit

家で余っているiPhoneをネットワークカメラにしてみました。ネットワークカメラを買うまでも無いけどちょっとしたときに使いたい。作業をしながら鍋の煮え具合を見たいときに使えます。配信用のサーバーを用意する必要もないので便利です。

iOSの画面をキャプチャして配信することもできるので社内でのUIテストとかいいかも知れないですね。ネットワークは社内や家庭内LANでの運用を想定しています。

動作イメージ

iPhoneでの配信画面
f:id:shogo4405:20160429064330j:plain

Safari(OS X)で視聴したところ
f:id:shogo4405:20160429064159p:plain

ソースコード

アプリ自体の公開の予定はありませんが、github(https://github.com/shogo4405/lf.swift/tree/master/lf )上でソースコードを公開しています。スニペットはこちら、

// Cocoapodを使う場合のインストール
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!

pod 'lf', '~> 0.2'
// frameworkのインポート
import lf

final class LiveViewController: UIViewController {
    var httpService:HTTPService!
    var httpStream:HTTPStream!

    override func viewDidLoad() {
        super.viewDidLoad()

        httpStream = HTTPStream()
  // attachCameraの代わりにスクリーンキャプチャーして送ることができるよ
        //httpStream.attachScreen(ScreenCaptureSession())
        httpStream.syncOrientation = true
        httpStream.attachCamera(AVMixer.deviceWithPosition(.Back))

        // helloを指定するとhttp://ip.addr:8080/hello/playlist.m3u8 としてアクセスできるようになる。
        httpStream.publish("hello")

        httpService = HTTPService(domain: "", type: "_http._tcp", name: "lf", port: 8080)
        httpService.startRunning()
        httpService.addHTTPStream(httpStream)

        view.addSubview(httpStream.view)
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        httpStream.view.frame = view.frame
    }
}

技術的なお話

ネットワークカメラ化にあたっては、HLS(HTTP Live Streaming)の配信環境を自前で構築することで実現しています。

  1. NSNetServiceクラスを利用したHTTPサーバーの実装
  2. カメラ映像をH264エンコードして.tsファイルを作成する
  3. HTTPサーバー経由での.tsを配信する

HLSの仕組みを利用することでHLS対応ブラウザー(Microsoft Edge, Safari, モバイChromeなど)で映像の視聴が可能になります。

NSNetServiceクラスを利用したHTTPサーバーの実装

NSNetServiceクラスを利用します。publishWithOptionsメソッドにNSNetServiceOptions.ListenForConnections指定することでサービスとして機能します。

// ポート番号: 8080, TCPとしてリスンする
var service:NSNetService = NSNetService(domain: "", type: "_http._tcp", name: "", port: 8080)
service.delegate = self
service.publishWithOptions(NSNetServiceOptions.ListenForConnections)

NSNetServiceDelegateプロトコルのnetService(sender: NSNetService, didAcceptConnectionWithInputStream inputStream: NSInputStream, outputStream: NSOutputStream)メソッドで、inputStreamとoutputStreamというソケット通信に利用するインスタントの組み合わせが得られるので初期化していきます。

カメラ映像をH264エンコードして.tsファイルを作成する

主にこのようなことをやって.tsファイルを作成していきます。ライブラリー公開先のAVMixerクラス, AVCEncoderクラス, TSWriterクラスで分担して処理を行っています。

  1. AVCaptureSessionを利用してCMSampleBufferを取得する
  2. VTCompressionSessionを利用して生の映像をH264エンコード処理する
  3. H264データをPacketizedElementaryStream化の後TransportStreamに変換して.tsとして書き出す

HTTPサーバー経由での.tsを配信する

HTTPリクエストに併せて.tsを返すコードを書き出すことでHLSの視聴が可能になります。 HLSのRFCを参考にしました。draft-pantos-http-live-streaming-06 - HTTP Live Streaming

プロ生ちゃんと一緒にiPhoneから生放送!

lf

iOS向けのライブ配信ライブラリーを書いてみた - Thousand Yearsで制作したRTMPなライブラリーGitHub - shogo4405/lf.swift: iOS用のライブ配信ライブラリーに映像効果を適用する機能をつけたので紹介します。
映像効果に利用するのは、プログラミング生放送でお馴染みのプロ生ちゃん(プロ生ちゃん(暮井 慧) | プログラミング生放送)です。プロ生ちゃんのpngとカメラ映像を合成して一緒に生放送できるようになります。
f:id:shogo4405:20160426124853p:plain

映像効果用クラスの作成

自作の映像効果を適用するためのVisualEffectクラスを継承したクラスを用意します。画像ファイルとカメラ映像を合成するための、主なポイントは以下の通り。

  1. CISourceOverCompositingのCIFilterの用意。ライブラリーの処理上CIImageになっているのでこちらのアプローチによる画像の合成を行います。
  2. カメラ画像(CIImageのextent)情報をもとにしてカメラ画像と同解像度の画像を作成する。この処理で出来た画像にプロ生ちゃん(合成したい画像)を描画しておく。
  3. プロ生ちゃんの画像をinputImage。カメラ映像をinputBackgroundImageとしてCIFilterを適用する
final class PronamaEffect: VisualEffect {
    // 1. CISourceOverCompositingなフィルターの準備
    let filter:CIFilter? = CIFilter(name: "CISourceOverCompositing")

    // 2. カメラ映像と同解像度の画像の作成
    var extent:CGRect = CGRectZero {
        didSet {
            if (extent == oldValue) {
                return
            }
            UIGraphicsBeginImageContext(extent.size)
            let image:UIImage = UIImage(named: "Icon.png")!
            image.drawAtPoint(CGPointMake(50, 50))
            pronama = CIImage(image: UIGraphicsGetImageFromCurrentImageContext(), options: nil)
            UIGraphicsEndImageContext()
        }
    }
    var pronama:CIImage?

    override init() {
        super.init()
    }
    // 3. フィルターの適用
    override func execute(image: CIImage) -> CIImage {
        guard let filter:CIFilter = filter else {
            return image
        }
        extent = image.extent
        filter.setValue(pronama!, forKey: "inputImage")
        filter.setValue(image, forKey: "inputBackgroundImage")
        return filter.outputImage!
    }
}

映像効果の適用

自作した映像効果を適用するコード

var effect:VisualEffect = VisualEffect()
var rtmpConnection:RTMPConnection = RTMPConnection()
var rtmpStream:RTMPStream = RTMPStream(rtmpConnection: rtmpConnection)
//  カメラソースの追加
rtmpStream.attachCamera(AVMixer.deviceWithPosition(.Back))

// -- エフェクトの登録
rtmpStream.registerEffect(effect)
// -- エフェクトの解除
// rtmpStream.unregisterEffect(effect)


// プレビュー映像の表示(映像効果が適用された状態で表示する)
view.addSubview(rtmpStream.view)

配信開始

// イベントハンドラの登録。接続が確立したことを確認するためにハンドラ内で行う
rtmpConnection.addEventListener(Event.RTMP_STATUS, selector:#selector(HogeController.rtmpStatusHandler(_:)), observer: self)

// サーバーへの接続
rtmpConnection.connect("rtmp://path/to/server")

func rtmpStatusHandler(notification:NSNotification) {
    let e:Event = Event.from(notification)
    if let data:ASObject = e.data as? ASObject , code:String = data["code"] as? String {
        switch code {
        case RTMPConnection.Code.ConnectSuccess.rawValue:
             // 配信開始!
             rtmpStream!.publish("streamName")
        default:
             break
        }
    }
}

ソースコード

説明のために端折って書いています。ソースコードの全部はこちらで参照できます。
lf.swift/LiveViewController.swift at master · shogo4405/lf.swift · GitHub

[iOS] iOS8のエミュレーターでCIImage(CVPixelBuffer: buffer) がnil に

カメラの映像にフィルターかけるときに使うCIImage(CVPixelBuffer: buffer)がエミュレーターで動作しなかったのでメモ。実機では動作した。

- 実機 エミュレーター
iOS8.x
iOS9.x

iOS向けのライブ配信ライブラリーを書いてみた

iOS向けのRTMPライブ配信ライブラリーを書きました。iPhone搭載のカメラをつかったライブ配信ができます。ライセンスは修正BSDライセンスgithubのほうに公開しています。RTMPでのライブ配信をサポートしています。https://github.com/shogo4405/lf.swift
iOS9が出る頃には1.0.0で正式リリースしたいなぁ。

機能概要

  • RTMPの配信
  • AMF0の一部分をサポート
  • ハードウェアをつかったH264, AACエンコーディング
    • と書けば聞こえがいいけどAVAssetWriterつかっているだけですので...

サンプルコード

こちらに置いてます。https://github.com/shogo4405/lf.swift/tree/master/lfSample
ViewController.swiftの冒頭をrtmpのパス名。ストリーム名をセットすれば配信できます。RTMP向けのサーバーが別途必要になります。

    let url:String = "rtmp://192.168.179.4/live"
    let streamName:String = "test"

視聴は?

視聴もと考えたのですがAppStore準拠するためには、3G環境下でHLSの要件が必須です。配信を先につくりました。最新のWowzaやFMSだとRTMP受け。HLS配信ができるのでそちらに任せます。

[misc] Google Codeからプロジェクト移転の設定

Google Codeのプロジェクトページにアクセスしたときに転送ができる機能がありました。検索からGoogle Codeページに飛んで、このプロジェクトはgithubに移転しました!というメッセージでリンクを辿るということをしなくても良さそうです。

設定方法

移転後は全てのアクセスが移転先に転送されます。したがって先にチケットやソースの移転を済ませておきます。

  1. [Administer]→[Advanced]でのProject movedの項目に移転先のURLを入力します
  2. 今回のケースでは、https://code.google.com/p/kanaxs/の移動設定だったのhttps://github.com/shogo4405/kanaxsを入力しました。
  3. 入力後、Project movedのボタンを押下する

以上で手続きは完了です。https://code.google.com/p/kanaxs/へのアクセスがすべからくhttps://github.com/shogo4405/kanaxsに転送されるようになりました。
是非、Google Codeでのプロジェクト管理者の人は検索迷子を出さない為にこの設定をしていただけると幸いです。

移動設定を戻す場合

https://code.google.com/p/${project}/adminAdvanced のページは生きているので該当ページにある[Publish project]で復旧できます。

[zf2] リクエストの拡張子別にviewにも対応したテンプレートを適用する

http://d.hatena.ne.jp/shogo4405/20141109/1415519613 の内容を応用してjsonに対するリクエストあればControllerに対応するjsonをテンプレートを変更すること可能になります。
/:controller[/:action][.format]とルーティングした際に、/view/controller/action.p{format}のテンプレートを選択するサンプルは次の通りです。

サンプルコード

<?php

namespace Application\Controller;

use Zend\Mvc\MvcEvent;
use Zend\View\Renderer\PhpRenderer;
use Zend\EventManager\EventInterface;
use Zend\EventManager\EventManagerInterface;

final class MimeListener extends \Zend\EventManager\AbstractListenerAggregate
{
    private static $mimeTypeCriteria = [
        'atom' => 'application/atom+xml',
        'json' => 'application/json',
    ];

    public function attach(EventManagerInterface $e)
    {
        $this->listeners[] = $e->attach(MvcEvent::EVENT_DISPATCH, [$this, 'onDispatch']);
        $this->listeners[] = $e->attach(MvcEvent::EVENT_RENDER, [$this, 'onRender'], -10000);
    }

    public function onDispatch(MvcEvent $e)
    {
        $controller = $e->getTarget();
        $sm = $controller->getServiceLocator();

        $format = $e->getRouteMatch()->getParam('format', '');
        if (!array_key_exists($format, self::$mimeTypeCriteria)) {
            return;
        }

        $stack = $sm->get('ViewTemplatePathStack');
        $stack->setDefaultSuffix('p' . $format);
        $controller->layout('layout/empty');

        if ($e->getResponse()->getStatusCode() === 404) {
            $view = $e->getViewModel();
            $view->setTemplate('error/404.' . $format);
        }
    }

    public function onRender(MvcEvent $e)
    {
        $format = $e->getRouteMatch()->getParam('format', '');
        $headers = $e->getResponse()->getHeaders();

        if (!array_key_exists($format, self::$mimeTypeCriteria)) {
            return;
        }

        $headers->addHeaderLine('Content-Type', self::$mimeTypeCriteria[$format]);
    }
}
<?php
final class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        $application = $e->getApplication();

        $moduleRouteListener = new ModuleRouteListener();
        $eventManager = $application->getEventManager();
        $serviceManager = $application->getServiceManager();

        $eventManager->attachAggregate(new MimeListener());
        $moduleRouteListener->attach($eventManager);
    }
}

何がうれしいかと言うと

都度、action毎に返すformatを見てごにょごにょしないといけないのをやらなくて済むようになります。

// 適用前
function hogeAction()
{
  $format = $this->params()->fromRoute('format', false);
  if ($format === 'json') {
      return new JsonModel();
  } else if ($format === 'atom') {
      return new FeedModel();
  }
  return new ViewModel();
}

// 適用後
function hogeAction() {
  return [
    'content' => 'hello world!!'
  ]
}

[zf2] Viewのデフォルトの拡張子phtmlを任意に変更する

zf2でviewの拡張子をphtmlから任意に変更したかったのでその設定のメモです。今回のケースではphtmlからphpに変更してます。

module.config.php

configベースは次の通り

<?php
return [
    'view_manager' => [
        'default_template_suffix' => 'php',
    ]
]

コードベース

コードベースだと次の通り

<?php
// $sm はServiceManagerのインスタンス
$stack = $sm->get('ViewTemplatePathStack');
$stack->setDefaultSuffix('php');