文字っぽいの。

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

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)が得意・興味がある皆さんはぜひ聞きに来てください!そしていろいろお話しましょう!

#esa のURLを展開してくれるKujakuで、コメントも展開できるようになりました。

Kujakuとは

Closedなesaの記事URLをSlackに貼ったら展開してくれるやつです。詳しくはこちら

inside.pixiv.blog

今回のアップデート

2つアップデートがあります。

コメントが展開できるようになりました

f:id:FromAtom:20190806172830p:plain

今まではコメントのリンクを貼り付けたとしても、コメントされた記事自体が展開処理されていました。今回のアップデートで、コメントのリンクの場合はちゃんとコメントが展開されるようになります。

footerが読みやすくなりました

f:id:FromAtom:20190806173043p:plain

見やすくなりました。この変更は @sue445 さんにPull Requestを送っていただいて実現しました。ありがとうございます!

Swiftで配列の要素を安全に取得する。

毎回忘れるのでブログに書く。配列の要素を取得するとき、

if 0 <= index && index < array.count {
    let item = array[index]
}

のようなコードを書くと思う。ちょっと頭いい感じだと

if array.indices.contains(index) {
    let item = array[index]
}

とも書ける。ただこれはめんどくさいので、こういった拡張を書いておくと良い。

import Foundation

extension Collection {
    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

こうすると

if let item = array[safe: index] {
    // ここでitemを使う
}

という書き方ができる。

自分にとって簡単な仕事が、他人にとっても簡単とは限らないという話。

半年ぐらい前、弟が使わなくなったパソコンを譲り受ける為にレンタカーを走らせている時、こういった相談を受けた。

「自分なんかが回せる仕事なんだから、他の人でも簡単にできると思っていたのに上手くできないことが多い。その人が無能だとは思わないのだけど、なぜなんだろうか。」

非常にわかる。自分も昔は同じような考え方をしていた。謙遜ではなく自己評価が低い人間なので「凡人である自分なんかでもできる仕事」は「誰でもできる仕事」なのだと考えていた。しかし、こういった考えは一方で正しく、一方で間違っているのだ。

あまり好きな概念ではないが、人には必ず向き不向きがある。身体が大きければバレー・バスケ・柔道部にスカウトされ、勉強ができれば「将来は学者さんかしら」と言われるような*1世間で育ってきた人からすると、多くの向き不向きは分かりやすい先天的差異に起因すると考えがちである。他にも、性格やコミュニケーション能力から「営業向き」とか「職人向き」と言われたことも多いだろう。こういった向き不向きという言葉の使い方を自分はあまり好かないのだけど、現実問題としてはもっと小さく向き不向きが遍在している。

例えば、ITエンジニアには1日中パソコンをいじっていることが苦痛だと感じない人が多い(多かった)と思う。もちろん目や腰が疲れることはあるが、そういった要因を除けば非常にストレスは少ないだろう。この時点ですでにITエンジニアとして「向いている」のである。「そんなに小さなことで?」と思うかもしれないが、こういった小さな向き不向きが積み重なることで大きな個性を生んでいる。他にもキーボードでのタイピングや文字ベースでのコミュニケーションが苦痛ではないとか、日本語ではないWebサイトを見た時にすぐに諦めてブラウザバックしなかったり、検索(GoogleでもTwitterでもInstagramでも)で適切なクエリを組み立てて望みの情報を引き出せるというのも「向いている」特徴だと思う。これらは「リテラシーが高い」と言い換えることもできるだろう。「自分には簡単に思える仕事が、実は他人には難しい」という事象は、この向き不向きに起因していると考えている。

とても雑な例を示そう。電話をかけることが苦手な "電話苦手くん" と、特に何も感じず電話ができる "電話普通くん" がいる。ここで後者の "電話普通くん" から "電話苦手くん" へ「今度パーティやるから、この5つの会場候補に電話して会場あいてるかと予算どのくらいか聞いておいて」という仕事が依頼されたとする。 "電話普通くん" からすると「5箇所に電話してまとめれば良いだけ」の非常に簡単な仕事である。

さて "電話苦手くん" にとっては、これは非常に気が重い仕事になる。予め聞きたいことをメモにまとめて、深呼吸して覚悟を決めてから電話をかける必要がある。コールの間なんども会話内容を脳内でシミュレートして、留守番電話だったり通話中だったりすると安心するぐらいにはビクビクだ。そして、とにかく電話は嫌なのでWeb上でひたすら調べて情報を集めることになる。それが古い個人ブログ記事だったとしても、電話一本かけて数分で終わる調査だったとしてもだ。こうなると途端に仕事は遅くなる。遅くても1時間もしたらまとめ終わるはずが、数時間経っても報告が来ない。 "電話普通くん" からすると「5回電話をかけるだけの誰にでもできる簡単な仕事なのにどうしたんだ?」となる。

この例は分かりやすさの為に極端にしたが、こういった向き不向きの細かい差異は実にたくさんあることに気がつくと思う。ある仕事が苦ではないというのは、非常に大きな才能のひとつだ。このことは、任天堂元代表取締役社長 岩田 聡 氏も下記のように話している。

自分の労力の割に周りの人がすごくありがたがってくれたり,喜んでくれたりすることってあるじゃないですか。要するにね,「それがその人の得意な仕事なんだ」って話で。逆に,自分的にはすごい努力して,達成感もたっぷりあるのに,周りからは「はあ?」みたいに思われることもあって。それはね,本人が好きだったとしても,実は不得意なことかもしれないんですよ。

任天堂・岩田氏をゲストに送る「ゲーマーはもっと経営者を目指すべき!」最終回――経営とは「コトとヒト」の両方について考える「最適化ゲーム」

引用文中の言葉を借りると『得意な仕事』をしているとき、自分目線ではとても楽に感じるため、誰でもできる簡単な仕事をしていると思い込んでしまう。しかし、例えば同じ仕事をA君に依頼したときに、なぜかスムーズに完遂できず、「どうしてこんなこともできないんだろう?」と悩むことになる。このとき、その仕事がA君に「向いてない」と考える前に、自分が「向いている」だけかもしれないと意識することが重要だ。向いている仕事は低コストで高パフォーマンスを得ることができる。そのため、誰でもできる簡単な仕事だと錯覚してしまう。しかし多くの場合では、A君が「向いてない」ということはなく、単純に経験や知識が不足していたり、指示や指導が足りてないということのほうが多い。そういった原因を潰してから最後に「向いてないのかもしれない」と考えるべきだ。

ここまで長々と書いてきたが、結論として伝えたかったのは「自分が簡単だと思っている仕事が、実は他人にとっては簡単ではないかもしれないことを留意すべき。」ということである。もちろん、「自分にしかできない仕事だ」という自惚れを持ってはいけない。向き不向きという観点でその仕事に向いてる人は多く存在しており、その中で才能や努力の多寡による優劣は存在していて、別軸の話だからだ。自信を持つのは良い。自分にとっては特別ではない才能で他人に喜んでもらえるという体験によって、自己評価を高めていくことは非常に重要である。

ちなみに、別の話になるけども「自分は向いていない」と思いこんでいた領域でも「実は向いている」ということがよくある。「人と話すのは苦手だけど、新人教育は得意」という人がいる。一見矛盾しているが、雑談のような高速で高度なコミュニケーションは苦手だけど、しっかりと資料をまとめて話したり、1on1などでテーマを決めて話して悩み・特性をヒアリングするのは得意とかよくある話。人間は自分で自分を正しく分析することが苦手なので、雑談への苦手意識から「人間との対話は全て苦手」だと勘違いしがちなのである。

最後に、仕事ではなるべく「向いていて好きな」ことに特化していくのが良いと思う。当人としては比較的楽な仕事なのに、周りからは感謝されるし頼られるし評価もされやすい。ただし、領域の向き不向きは、その人の好き嫌いとは関係がないこともよくある。「好きだけど向いていない」仕事は熱意でカバーできるのでなんとかなることが多いが、できれば「向いていて好きな仕事」でお金をもらえることが一番良いと思う。

*1:こういった話も今や身体特徴に起因する差別的な話ともとられるだろう

App Store ConnectでAdmin権限を持っているのに、他のユーザーをAdminにできないときの対処法

Adminにしようとチェックマークを入れて、保存ボタンをクリックすると

f:id:FromAtom:20190711193336p:plain

こんな感じで、

ご利用のアカウントには他のユーザを編集する権限がありません。詳細については、お客様のチームエージェントにお問い合わせください。

と出てきてAdminに変更できない。そんなときは少し下の方にある

f:id:FromAtom:20190711193442p:plain

このデベロッパリソースのチェックマークを入れるとAdminに変更できます。

めでたしめでたし。