文字っぽいの。

文字を書いています。写真も混ざります。

Slackのカスタム絵文字をesaにらくらくコピー!「Utsushie(写し絵)」シリーズを作りました。

f:id:FromAtom:20200204114423p:plain

はじめに

企業やグループでSlackを使っていると、カスタム絵文字をいっぱい登録しますよね。そしてesaも使っていると、Slackで登録したカスタム絵文字と同じものがesaでも使いたくなってきます。

そこで、Slackに登録されたカスタム絵文字をかんたんにesaにもコピーできる「Utsushie(写し絵)」シリーズを作りました。この記事ではUtsushieシリーズの紹介をしたいと思います。

Utsushieシリーズ

Utsushieシリーズには

  • Utsushie
  • Utsushie-Stream

の2つがあります。それぞれできることや使い所が違うので解説していきます。

Utsushie

github.com

UtsushieはSlackに登録されているCustom EmojiをesaにコピーするCLIツールです。なお、esaのカスタム絵文字仕様 に準拠していないCustom Emojiは無視されます。

「とにかく一気にSlackからesaに絵文字をコピーしたいんじゃ!」というときに便利です。また、esa上でにすでに登録されているカスタム絵文字を全削除したあとでコピーをする --clean や、 コピーや画像のダウンロードが一切走らない --dry-run オプションも用意してあります。

Utsushie-Stream

github.com

Utsushie-Stream はSlackの emoji_changed event に反応して、Slackに登録されたEmojiをesaにコピーするツールです。反応するイベントは

  • 絵文字の追加
  • aliasの追加
  • 絵文字の削除

の3つです。これらのイベントに対応してesaにも絵文字やaliasを追加したり、削除したりしてくれます。ただし、イベントベースで処理が発生するので、すでにSlackに登録されている絵文字はコピーされません。

また、絵文字のコピー・削除が発生時にはSlackのチャンネルに

f:id:FromAtom:20200204130744p:plain

と通知が飛んできます。esaのフォーマットに適合していない絵文字( :テストです: )の場合は

f:id:FromAtom:20200204130953p:plain

といった形でエラーも通知してくれます。

おすすめの使い方

  1. UtsushieでSlackからesaに絵文字をすべてコピーする
    • Slackの絵文字量によってはすごい時間がかかります
  2. Utsushie-Streamをセットアップする

という手順で環境を整えておくと、自動でSlackとesaでカスタム絵文字の状態を同期されるようになります。ただし、esaのカスタム絵文字仕様 に反する絵文字はコピーされませんので注意してください。

なお、それぞれのツールの使い方は、GitHub RepositoryのREADMEに書いてありますので、そちらをご参照ください。

よもやま話

絵文字(Emoji) を 移す(Utsusu)からUtsushiEです。写し絵か影絵かすごい悩んですが、影絵だとKageeになって、ケギーとしか読めなかったのでやめました。

ただ、OSSロゴに利用しているのは影絵の手遊びの代表的な存在であるキツネです。写し絵っぽいアイコン思いつかなかったんですよね。映写機だと動画制作ツール感が出てしまって。あと、本当はキツネの形した手のイラストが使いたかったんですが、そんな都合の良い素材はなかったのでシルエット感が強いキツネの素材を使わせてもらいました。

元ネタ

このツールの元ネタは esa5周年パーティ にて

次に、ぷりんたい (@spacepro_be)さんより、SmartHRさんの社内で使われているカスタムemojiをslackとesaで連携するツールについて、ツールを作った @yamashushさんに代わって、紹介していただきました。

といったLTで聞いた話です。とても便利そうで自分も使いたかったのですが、OSSとして公開される霊圧を感じられなかったので「待ってないで自分で作るかー」とエイヤで作った次第です。実は気づかないうちに公開されてたりするんだろうか……?

まとめ

Slackからesaに簡単にカスタム絵文字をコピーできるツール「Utsushie」と「Utsushie-Stream」を作りました。ぜひ、ご利用ください。 (\( ⁰⊖⁰)/)

Swift5で正規表現を書く時に "\" を2回書きたくないときはRaw String Literalを使おう。

問題

Swiftで正規表現を書くと

let regex = "\\A\\s*[-+*]\\s*\\S+\\z"  // Markdownの箇条書きか判定する雑な正規表現

と書かないといけない。これでは不便ですね。

解決方法

Swift 5からRaw strings literalの仕組みが導入されました。このliteralを利用すると

let regex = #"\A\s*[-+*]\s*\S+\z"#  // Markdownの箇条書きか判定する雑な正規表現

と書けます。 #"ここに文字が入る"# という記法です。これで、わかりやすく正規表現を記述することができますね。

余談

また、このliteral内では " も利用することが可能で

let string = #"The word "bookstore" is a compound consisted of "book" and "store.""#
// => The word "bookstore" is a compound consisted of "book" and "store."

この様に文字列内で " をそのまま利用できます。また、文字列内で "# を使いたい場合は

let string = ##"The word "bookstore"#hashtag"##
// => The word "bookstore"#hashtag

この様に、前後の #を増やしてあげれば良いです。

さらに、文字列補間もしたいですよね。そういう場合は

let name = "太郎"
let string = #"犬の名前は "\#(name)" です。"# // => 犬の名前は "太郎" です。

\#(name) とすることで利用できます。

XiaomiのMi band 4を買った

f:id:FromAtom:20200111175358j:plain

電車通勤になってから、携帯を取り出すのではなく腕時計で時間を確認したいと思うことが増えた。 せっかくなのでスマートウォッチというのが欲しいが、いかんせんAppleWatchは高すぎる。やりたいこととしては、

  • 時間がわかる
  • スマホに通知が来た時に分かって、ちょっとだけ内容がわかる
  • iOSと連携できる

くらい。睡眠やアクティビティの計測とか、音楽の操作はあったら嬉しいけど多分使わないからいいやという温度感。 そんな時に、このMi band4を見つけた。

良い点

  • とにかく安い4000円弱
  • 別売りのバンドも10色セットで1500円と安い
    • 個別でも買えるけどすこし割高になる
  • カラー液晶でタッチパネルになっている
  • 小さいし軽い
  • バッテリーが20日も持つ
    • 2日使ってて4%しか減らなかったので、確かに20日持ちそう感がある
    • 週末に充電しておけばいいので使いやすい
  • サードパーティ製のfaceが使える。自分でface作ったりもできる。
    • f:id:FromAtom:20200111174431j:plain
  • UIは英語しか無いが、通知内の日本語はちゃんと表示される
  • 心拍数、睡眠、運動のトラッキングもできる
    • 精度は不明

微妙な点

  • 公式が提供しているfaceがダサい
  • なおかつAndroidじゃないとサードパーティ製の文字盤を使えない
    • iOSではデフォルトが一番いいと思います
    • 追記:iOSでもAmazToolsというアプリを使うとできるとの情報をもらいました
  • 一度変更したfaceをデフォルトに戻す方法が全然わからない
    • ペアリングを切ると元に戻る
    • これ以外の方法がわからん
  • 設定用アプリは日本語だけど、Mi band4側のUIは英語だけなので苦手な人は苦手そう
  • バンドから本体を取り外さないと充電できない
    • 一応、非公式でクリップ式の充電アダプタが出ている

高スペックなのに低価格。一体何がどうしてこんなに安いのか分からないけど、欲しかった機能が必要十分にあるので大変便利。良い買い物でした。

Macのzshで時間がかかる処理が終わったら通知してくれるようにする

はじめに

開発をしていると、テストやセットアップ処理、ライブラリの導入などで長い時間待つことがあると思います。 そんな時に暇すぎてTwitterを見に行くと、そのまま夕方になり生産性が破滅することが良くあると思います。

なので長い処理が終わったら、通知が来るようにしてTwitterランドから仕事に戻れるようにしました。

動かした環境

利用するもの

  • terminal-notifier
  • zplug
  • zsh-notify or iterm-notify

手順

では準備していきましょう。

terminal-notifierをインストールする

こいつはターミナルからmacOSの通知を出してくれる君です。Homebrewで入れていきます。

brew install terminal-notifier

インストールしたら下記のコマンドを試してみましょう。

terminal-notifier -message 'HELLO'

そして下記のように通知が来たら成功です。

f:id:FromAtom:20191210193602p:plain

さて、これで現状でも

処理が長いコマンド | terminal-notifier -message '終わったよ'

などとすればコマンドの処理が終わった後に通知が来ます。 しかし、大体の場合 "処理が長いコマンド" を実行したあとで「あ!通知設定するの忘れてた」となると思います。 これを防ぐため、一定以上実行に時間がかかった処理が終わったら通知が来るようにする zsh-notify を入れます。 の、前に、 zsh-notify のようなプラグインを管理してくれる zplug を導入します。

zplugをインストールする

これもHomebrewで簡単に入ります。

brew install zplug

zsh-notifyをインストールする

.zshrc に下記の処理を追記します。

export ZPLUG_HOME=/usr/local/opt/zplug
source $ZPLUG_HOME/init.zsh

zplug "marzocchi/zsh-notify"

if ! zplug check --verbose; then
    printf "Install? [y/N]: "
    if read -q; then
        echo; zplug install
    fi
fi

zplug load

これを書いたあとでzshを再起動すると下記のように、プラグインのインストールを促してくれます。

f:id:FromAtom:20191210195251p:plain

ここで y を入力すれば、zsh-notify がインストールされます。

試してみる

試しに下記のコマンドを実行してみましょう。なお、ターミナルにフォーカスが当たってると通知が来ないので注意です。

sleep 40

下記のように通知が来たらOKです。

f:id:FromAtom:20191210200811p:plain

通知される時間を変える

デフォルトでは30秒以上の処理で通知が来ますが、この時間を変えたい場合は .zshrc に、

 zstyle ':notify:*' command-complete-timeout 15

みたいに追記しましょう。これで15秒がしきい値になります。

iTerm2をご利用の方へ

iTerm2を利用している方は marzocchi/zsh-notify ではなくて marzocchi/iterm-notify が用意されているので、そちらを利用するのが良いでしょう。

CMSampleBuffer を Resize する

はじめに

CMSampleBuffer をResizeしてサイズを小さく(大きく)したいことがあると思います。試していませんが、途中で CIImage になっているので、回転、変形、加工などCIFilterで行う処理を適用できると思います。

環境

  • Xcode11.1
  • Swift 5.1

手順まとめ

  1. CMSampleTimingInfoを保存しておく
  2. CMSampleBufferをCIImageに変換する
  3. CIImageをResizeする
  4. ResizeしたCIImageをCVPixelBufferに変換する
  5. CVPixelBufferをCMSampleBufferに変換する

手順詳細

1.CMSampleTimingInfoを保存しておく

import ReplayKit

final class Sample {
    let timingInfo: CMSampleTimingInfo

    init(sampleBuffer: CMSampleBuffer) {
        let presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
        let duration = CMSampleBufferGetDuration(sampleBuffer)
        let decodeTimeStamp = CMSampleBufferGetDecodeTimeStamp(sampleBuffer)
        timingInfo = CMSampleTimingInfo(duration: duration, presentationTimeStamp: presentationTimeStamp, decodeTimeStamp: decodeTimeStamp)
    }
}

2.CMSampleBufferをCIImageに変換する

private func generateCIImage(from sampleBuffer: CMSampleBuffer) -> CIImage? {
    guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
        return nil
    }
    
    let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
    return ciImage
}

3.CIImageをResizeする

private func resizeCIImage(from ciImage: CIImage, scale: CGFloat) -> CIImage? {
    guard let filter = CIFilter(name: "CILanczosScaleTransform") else {
        return nil
    }

    filter.setDefaults()
    filter.setValue(ciImage, forKey: kCIInputImageKey)
    filter.setValue(scale, forKey: kCIInputScaleKey)
    return filter.outputImage
}

【追記】

こちらの処理は、CoreImage.CIFilterBuiltins - cockscomblog?の記事を参考にすると、もう少しスッキリ書けそうです。

4.ResizeしたCIImageをCVPixelBufferに変換する

private func generateCVPixelBuffer(from ciImage: CIImage) -> CVPixelBuffer? {
    let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary
    var pixelBuffer: CVPixelBuffer!
    let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(ciImage.extent.size.width), Int(ciImage.extent.size.height), kCVPixelFormatType_32BGRA, attrs, &pixelBuffer)
    guard status == kCVReturnSuccess else {
        print("CVPixelBufferCreateに失敗")
        return nil
    }

    let ciContext = CIContext()
    ciContext.render(ciImage, to: pixelBuffer, bounds: ciImage.extent, colorSpace: CGColorSpaceCreateDeviceRGB())
    CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
    
    return pixelBuffer
}

5.CVPixelBufferをCMSampleBufferに変換する

この際に、保持しておいた CMSampleTimingInfo を利用します。

private func generateCMSampleBuffer(from cvPixelBuffer: CVPixelBuffer) -> CMSampleBuffer? {
    var sampleBuffer: CMSampleBuffer?
    var timimgInfo: CMSampleTimingInfo = timingInfo
    var videoInfo: CMVideoFormatDescription!
    CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: cvPixelBuffer, formatDescriptionOut: &videoInfo)
    CMSampleBufferCreateForImageBuffer(allocator: kCFAllocatorDefault,
                                       imageBuffer: cvPixelBuffer,
                                       dataReady: true,
                                       makeDataReadyCallback: nil,
                                       refcon: nil,
                                       formatDescription: videoInfo,
                                       sampleTiming: &timimgInfo,
                                       sampleBufferOut: &sampleBuffer)

    return sampleBuffer
}

完成

これでResize処理をしたCMSampleBufferを生成することができます。

参考資料

RPBroadcastSampleHandlerで得られるCMSampleBufferのorientationを取得する

コード

override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
    if let orientationAttachment = CMGetAttachment(sampleBuffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil) as? NSNumber {
        if let orientation = CGImagePropertyOrientation(rawValue: orientationAttachment.uint32Value) {
                // orientationを使った処理
        }
    }
}

補足情報

#iOSDC Japan 2019で『スクリーン配信機能の実装が大変だったので知見をお伝えします』というお話をします。

f:id:FromAtom:20190905140010j:plain

今年もiOSDC Japanの季節がやってきましたね!ありがたいことに30分トークが採択されたので、登壇します!去年は、こういったタイトルで発表しました。

fromatom.hatenablog.com

去年は再生側をやったので今年は配信側です。こいつ、いっつも「大変だったので知見をお伝え」してんな。

プロポーザルには、

iOSで画面を収録してライブ配信を行うにはReplayKit2を利用し、Upload Extension経由で画面を配信する必要があります。 さて、そのUpload ExtensionをXcodeで追加すると BroadcastSetupViewController というUIViewControllerが追加されます。

「これ……なに……?」

なんとか謎のViewControllerの正体を暴いた後、次の壁にぶつかりました。 Upload Extensionの動作時にはiOS側の制限で約50MBのメモリ制限がかかっているため、 気楽に処理を書くとすぐにメモリが枯渇してしまうのです。

「気軽に処理書くとiOSに殺されるんだが……?」

このトークでは、将来スクリーン配信機能をつくる誰かが少しでも楽になることを主目的とし、 スクリーン配信機能の作り方をまとめながら、ハマりどころやデバッグのコツをお話します。

と書きました。プロポーザルにも書いてある様に、このトークでは

f:id:FromAtom:20190905135857p:plain

と言われて「突然スクリーン配信機能を作ることになった」際に手助けになることを目指しています。

登壇は、

  • 日時:2019年09月05日(木)17時50分〜
  • 会場:Track A

となっています。そうです、今日なんですよ。そして一発目です。露払いというとても緊張する枠になってしまいましたが、寝坊の危険性がなかったのはとても安心しています。

それでは会場でお会いしましょう!スクリーン配信(ReplayKit)が得意・興味がある皆さんはぜひ聞きに来てください!そしていろいろお話しましょう!