- 投稿日:2019-04-05T23:47:19+09:00
[iOS]UIBezierPath, CAShapeLayer, UIColor.init(patternImage:) でUITabBarの形、色、外観を変える
Swift4.2とSwift5 で動作確認済
https://itunes.apple.com/sg/app/pripara/id1039257927?mt=8
(香港のAppStoreじゃないとダウンロードできません)実現できたUITabBar(アイコンは今回の記事と関係ないです)
https://github.com/Satoru-PriChan/ChangeUITabBarAppearance
方法
⑴UIBezierPathを使って、どんなグラフィックを描きたいかを定義します。今回は、スタート地点のY座標をマイナスとし、縦に少し拡張しているだけです。
MyTabBarController.swiftfunc createLengthExpandShape() -> CGPath { let path = UIBezierPath() //Starting point (left top) then draw lines until the it returns to the starting point with close(). let startX: CGFloat = 0 let startY: CGFloat = -37 path.move(to: CGPoint.init(x: startX, y: startY)) path.addLine(to: CGPoint.init(x: self.tabBar.frame.width, y: startY)) path.addLine(to: CGPoint.init(x: self.tabBar.frame.width, y: self.tabBar.frame.height)) path.addLine(to: CGPoint.init(x: startX, y: self.tabBar.frame.height)) path.close() return path.cgPath }参考 https://medium.com/@philipp307/draw-a-custom-ios-tabbar-shape-27d298a7f4fa
(2)CAShapeLayerに(1)のパスを追加し、レイヤーの形を変えます。
MyTabBarController.swiftoverride func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. //中略 ここでUITabBarControllerの他の設定を色々することになると思います //Change tabbar shape self.changeTabBarShape() } func changeTabBarShape() { let shapeLayer = CAShapeLayer() //Get CGPath shapeLayer.path = self.createLengthExpandShape() //Set image shapeLayer.fillColor = UIColor.init(patternImage: UIImage.init(named: "MyImage.png")!).cgColor //Set layer self.tabBar.layer.insertSublayer(shapeLayer, at: 0) }(3)UIColor.init(patternImage:) でセットしたい画像を用意し、Xcodeにドラッグ&ドロップで追加してください。その際Copy Items If Neededにチェックを入れることをお勧めします。
画像の準備が一番わかりにくい点です。
画像はランタイム時上下逆にして使われるので、画像編集ソフトなどで画像の内容をあらかじめ上下逆にしておいてください。また画像が貼られ始めるのは「拡張前の」レイヤーの左上角からという点に注意してください。今回、画像下のピンク色で雑に塗りつぶしただけの部分がUITabBarの本来の左上から貼られます。
その上のCAShapeLayerで拡張した部分には、画像上のレースのひらひら部分が貼られます。
ひらひら部分と、塗りつぶしただけの部分の間をpngで透過しておくと、UITabBarが画像に合わせて複雑に形を変える(ように見える)ため、デザイン性が増します。
CAShapeLayerの拡張法をもっと複雑に変えたい場合は、画像の中身も調整する必要があると思います。今回は縦長にしただけなので、簡単に済んでいます。
全プロジェクト
https://github.com/Satoru-PriChan/ChangeUITabBarAppearance気に入ったら、GitHubの方に星もらえると助かります。
もっとエクセレントなやり方があると思うので、誰か教えてください・・・
以上
- 投稿日:2019-04-05T23:10:03+09:00
iOSの照度センサーを使って画面を暗くしたり明るくしたりする
iOSの照度センサーを使って、なにかできないか模索していたときのメモ
iPhoneのカメラ部分を触ると画面を暗くできる
import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // 近接監視が有効(true)か無効かを示すブール値 UIDevice.current.isProximityMonitoringEnabled = true //照度センサーを監視 NotificationCenter.default.addObserver(self, selector: Selector("proximityChanged"), name:UIDevice.proximityStateDidChangeNotification, object: nil) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } @objc func proximityChanged() { //状態を表示 print("\(UIDevice.current.proximityState)") } }
- 投稿日:2019-04-05T19:48:58+09:00
FirebaseをCarthageで設定する時のXcodegenのyml
こちらを参考にxcodegenのymlを書きました。
https://qiita.com/lovee/items/09d33b7c8b1e3ff3dcc6targets: App: settings: base: OTHER_LDFLAGS: $(inherited) $(OTHER_LDFLAGS) -ObjC dependencies: - framework: Carthage/Build/iOS/Firebase.framework embed: false link: false - framework: Carthage/Build/iOS/FIRAnalyticsConnector.framework embed: false - framework: Carthage/Build/iOS/FirebaseAnalytics.framework embed: false - framework: Carthage/Build/iOS/FirebaseAuth.framework embed: false - framework: Carthage/Build/iOS/FirebaseCore.framework embed: false - framework: Carthage/Build/iOS/FirebaseCoreDiagnostics.framework embed: false - framework: Carthage/Build/iOS/FirebaseInstanceID.framework embed: false - framework: Carthage/Build/iOS/GoogleAppMeasurement.framework embed: false - framework: Carthage/Build/iOS/GoogleUtilities.framework embed: false - framework: Carthage/Build/iOS/GTMSessionFetcher.framework embed: false - framework: Carthage/Build/iOS/nanopb.framework embed: false - sdk: libc++.tbd - sdk: libsqlite3.tbd - sdk: CoreTelephony.framework - sdk: StoreKit.frameworkFirebaseAnalyticsとAuthのサンプル
- 投稿日:2019-04-05T18:14:38+09:00
【Swift】オセロ(リバーシ)
概要
友人に 本気で始めるiPhoneアプリ作り という本を頂いたので、Swiftでオセロゲームを作りました。このページで紹介するものはオフライン対戦です。サーバーとの通信を実装したものは他のページで紹介します。
環境
OS
・ macOs
開発環境
・ Xcode10
・ Swift4完成画像
ファイル一覧
この 4 ~ 6 の画像を使ってオセロ盤を作ります。
難しいところはないと思うのでコメントを読んで理解してください。わからないところがあればコメントお願いします。記事を修正します。
board.swift
board.swiftimport Foundation class Board{ var SIZE: Int = 0 let DIRECTIONS_XY = [[-1, -1], [+0, -1], [+1, -1], [-1, +0], [+1, +0], [-1, +1], [+0, +1], [+1, +1]] let BLACK = -1 let WHITE = 1 let BLANK = 0 var square:[[Int]] = [] // オセロを開始する際に呼ばれ、オセロ盤を初期化する func start(size: Int){ self.SIZE = size let center = size / 2 for _ in 0..<self.SIZE{ var array:[Int] = [] for _ in 0..<self.SIZE{ array += [BLANK] } square += [array] } square[center-1][center-1] = self.WHITE square[center-1][center] = self.BLACK square[center][center-1] = self.BLACK square[center][center] = self.WHITE } // 盤上にある石の個数を返す func returnStone() -> (Int,Int) { var black = 0 var white = 0 var blank = 0 for y in 0..<SIZE{ for x in 0..<SIZE{ switch square[y][x]{ case BLACK: black += 1 case WHITE: white += 1 default: blank += 1 } } } return (black, white) } // 対戦終了時もう一度対戦する際にボード板をリセットする func reset(){ var _square:[[Int]] = [] let size = SIZE let center = size / 2 for _ in 0..<SIZE{ var array:[Int] = [] for _ in 0..<SIZE{ array += [BLANK] } _square += [array] } _square[center-1][center-1] = self.WHITE _square[center-1][center] = self.BLACK _square[center][center-1] = self.BLACK _square[center][center] = self.WHITE square = _square } // ボード盤を返す func return_board() -> [[Int]]{ return square } // 呼ばれた段階で Game Over であるかどうかを判定する func gameOver() -> Bool { var black = 0 var white = 0 var blank = 0 for y in 0..<SIZE{ for x in 0..<SIZE{ switch square[y][x]{ case BLACK: black += 1 case WHITE: white += 1 default: blank += 1 } } } if( blank == 0 || black == 0 || white == 0 ){ return true } if( self.available(stone: BLACK).count == 0 && self.available(stone: WHITE).count == 0){ return true } return false } func is_available( x: Int, y:Int, stone: Int) -> Bool { if ( square[x][y] != BLANK ){ return false } for i in 0..<8 { let dx = DIRECTIONS_XY[i][0] let dy = DIRECTIONS_XY[i][1] if( self.count_reversible(x: x, y: y, dx: dx, dy: dy, stone: stone) > 0 ){ return true } } return false } // 引数で与えられた石の次に打てる場所を返す func available(stone: Int) -> [[Int]]{ var return_array:[[Int]] = [] for x in 0..<SIZE{ for y in 0..<SIZE{ if( self.is_available( x: x, y: y, stone: stone) ){ return_array += [[x,y]] } } } return return_array } // ボードに石を置く func put( x: Int, y:Int, stone: Int){ square[x][y] = stone for i in 0..<8 { let dx = DIRECTIONS_XY[i][0] let dy = DIRECTIONS_XY[i][1] let n = self.count_reversible( x: x, y: y, dx: dx, dy: dy, stone: stone) for j in 1..<(n+1){ square[x + j * dx][y + j * dy] = stone } } } func count_reversible( x: Int, y: Int, dx: Int, dy: Int, stone: Int) -> Int { var _x = x var _y = y for i in 0..<SIZE{ _x = _x + dx _y = _y + dy // 0 <= x < 4 : can't write <- Annoying!!!! if !( 0 <= _x && _x < SIZE && 0 <= _y && _y < SIZE ){ return 0 } if (square[_x][_y] == BLANK){ return 0 } if (square[_x][_y] == stone){ return i } } return 0 } }player.swift
今回は簡単なランダム君だけです。
player.swiftimport Foundation class Player { func play(board: Board, stone: Int) -> (Int,Int) { return Random(available: board.available(stone: stone)) } // 打てるところにランダムで打ちます。 // swift には random.choice() みたいなものありますか? func Random(available: [[Int]]) -> (Int,Int) { let int = Int.random(in: 0..<available.count) return (available[int][0], available[int][1]) } }ViewController.swift
ViewController.swiftclass ViewController: UIViewController { // BOARDSIZE で ボードのサイズを変更できます。 let BOARDSIZE = 8 var board = Board() var player = Player() var player_name = "Random" var Stone_count = 0 // -1: 黒 // 1: 白 let User_color = -1 let Cpu_color = 1 // ボードはボタンを行列配置して表現される var buttonArray: [UIButton] = [] // board.png, white.png, black.png let baseBoard = UIImage(named: "board") let white = UIImage(named: "white") let black = UIImage(named: "black") var resetButton = UIButton() var passButton = UIButton() var viewStoneCount = UILabel() // オセロ盤を表現するボタン class buttonClass: UIButton{ let x: Int let y: Int init( x:Int, y:Int, frame: CGRect ) { self.x = x self.y = y super.init(frame:frame) } required init?(coder aDecoder: NSCoder) { fatalError("error") } } // ボタンなどを生成 func createUI(){ board.start(size: BOARDSIZE) var y = 83 let boxSize = 84 / (BOARDSIZE/4) viewStoneCount.frame = CGRect(x: 18, y: 430, width: 330, height: 60) viewStoneCount.textAlignment = NSTextAlignment.center viewStoneCount.font = UIFont.systemFont(ofSize: 25) self.view.addSubview(viewStoneCount) for i in 0..<BOARDSIZE{ var x = 19 for j in 0..<BOARDSIZE{ let button: UIButton = buttonClass( x: i, y: j, frame:CGRect(x: x,y: y, width: boxSize,height: boxSize)) button.addTarget(self, action: #selector(ViewController.pushed), for: .touchUpInside) self.view.addSubview(button) button.isEnabled = false buttonArray.append(button) x = x + boxSize + 1 } y = y + boxSize + 1 } resetButton.frame = CGRect(x: 125, y: 575, width: 125, height: 45) resetButton.addTarget(self, action: #selector(ViewController.pushResetButton), for: .touchUpInside) resetButton.isEnabled = false resetButton.isHidden = true resetButton.setTitle("RESET", for: .normal) resetButton.titleLabel?.font = UIFont.systemFont(ofSize: 25) resetButton.setTitleColor(.white, for: .normal) resetButton.backgroundColor = UIColor(red: 0.3, green: 0.7, blue: 0.6, alpha: 1) resetButton.layer.cornerRadius = 25 resetButton.layer.shadowOpacity = 0.5 resetButton.layer.shadowOffset = CGSize(width: 2, height: 2) self.view.addSubview(resetButton) passButton.frame = CGRect(x: 150, y: 500, width: 80, height: 30) passButton.addTarget(self, action: #selector(ViewController.pushPassButton), for: .touchUpInside) passButton.isEnabled = false passButton.isHidden = true passButton.setTitle("Pass", for: .normal) passButton.titleLabel?.font = UIFont.systemFont(ofSize: 25) passButton.setTitleColor(.white, for: .normal) passButton.backgroundColor = UIColor(red: 0.3, green: 0.7, blue: 0.6, alpha: 1) passButton.layer.cornerRadius = 25 passButton.layer.shadowOpacity = 0.5 passButton.layer.shadowOffset = CGSize(width: 2, height: 2) self.view.addSubview(passButton) drawBoard() } // passButton を押された時の処理 @objc func pushPassButton() { CpuTurn() passButton.isEnabled = false passButton.isHidden = true } // resetButton を押された時の処理 @objc func pushResetButton() { board.reset() drawBoard() resetButton.isEnabled = false resetButton.isHidden = true passButton.isEnabled = false passButton.isHidden = true } // ボード盤をタッチされた時の処理 @objc func pushed(mybtn: buttonClass){ mybtn.isEnabled = false board.put(x: mybtn.x, y: mybtn.y, stone: User_color) drawBoard() if( board.gameOver() == true ){ resetButton.isEnabled = true resetButton.isHidden = false } self.CpuTurn() } // CPU func CpuTurn() { if( board.available(stone: Cpu_color).count != 0 ){ let xy = player.play(board: board, stone: Cpu_color) board.put(x: xy.0, y: xy.1, stone: Cpu_color) drawBoard() if( board.gameOver() == true ){ resetButton.isHidden = false resetButton.isEnabled = true } } if( board.gameOver() == true ){ resetButton.isHidden = false resetButton.isEnabled = true } if( board.available(stone: User_color).count == 0){ passButton.isHidden = false passButton.isEnabled = true } } // 画面にオセロ盤を表示させる func drawBoard(){ let stonecount = board.returnStone() viewStoneCount.text = "● Uer: " + String(stonecount.0) + " ○ CPU: " + String(stonecount.1) var count = 0 var _board = board.return_board() for y in 0..<BOARDSIZE{ for x in 0..<BOARDSIZE{ if( _board[y][x] == User_color ){ buttonArray[count].setImage(black, for: .normal) } else if( _board[y][x] == Cpu_color ){ buttonArray[count].setImage(white, for: .normal) } else { buttonArray[count].setImage(baseBoard, for: .normal) } buttonArray[count].isEnabled = false count += 1 } } let availableList = board.available(stone: User_color) for i in 0..<(availableList.count){ let x = availableList[i][0] let y = availableList[i][1] buttonArray[x*BOARDSIZE+y].isEnabled = true } } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. self.createUI() } }まとめ
kotlinと似ていてとても書きやすかったです。今回はランダム君としか対戦できませんが、サーバーと通信することで強いCPUと対戦することができます。それについては次回書きます。改善点や疑問点などがありましたらコメントお願いします。kotlin版もあとで書きます。
- 投稿日:2019-04-05T16:23:34+09:00
UIFeedbackGeneratorを簡単に使えるようにする
開発環境
- Xcode10.2
- Swift 5.0
UIFeedbackGeneratorとは
Apple公式から引用
概要
このクラスのインスタンスを自分でサブクラス化したり作成したりしないでください。代わりに、提供されている具象サブクラスの1つをインスタンス化してください。UIImpactFeedbackGenerator。影響が発生したことを示すには、影響フィードバックジェネレータを使用します。たとえば、ユーザーインターフェイスオブジェクトが何かにぶつかったり、所定の位置に固定されたときに、インパクトフィードバックを引き起こすことがあります。
UISelectionFeedbackGenerator。選択の変化を示すために選択フィードバックジェネレータを使用します。
UINotificationFeedbackGenerator。成功、失敗、および警告を示すために通知フィードバックジェネレータを使用します。
何かのアクションのフィードバックとしてユーザさんの端末をブルッてさせてフィードバックするやつです。
通常の実装方法
UIFeedbackGeneratorの使い方と便利に使えるライブラリから引用させていただきました。
class ViewController: UIViewController { private let feedbackGenerator: Any? = { if #available(iOS 10.0, *) { let generator: UIImpactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) generator.prepare() return generator } else { return nil } }() @IBAction private func light() { if #available(iOS 10.0, *), let generator = feedbackGenerator as? UIImpactFeedbackGenerator { generator.impactOccurred() } }各VCに記述するにはしんどいなという印象でした。
簡単に使う方法
以下の構造体でラップして利用します。
/// UIFeedbackGeneratorを簡単に利用するためのラッパー struct Feedbacker { static func notice(type: UINotificationFeedbackGenerator.FeedbackType) { if #available(iOS 10.0, *) { let generator = UINotificationFeedbackGenerator() generator.prepare() generator.notificationOccurred(type) } } static func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) { if #available(iOS 10.0, *) { let generator = UIImpactFeedbackGenerator(style: style) generator.prepare() generator.impactOccurred() } } static func selection() { if #available(iOS 10.0, *) { let generator = UISelectionFeedbackGenerator() generator.prepare() generator.selectionChanged() } } }使い方
フィードバックをさせたいところでstaticメソッドをコールします。
/// 例 override func viewDidLoad() { // ViewControllerの読み込みが終わったらフィードバック Feedbacker.notice(type: .success) }コールの仕方は以下になりますので用途によって使い分けてください。
// notice Feedbacker.notice(type: .success) Feedbacker.notice(type: .warning) Feedbacker.notice(type: .error) // impact Feedbacker.impact(style: .heavy) Feedbacker.impact(style: .light) Feedbacker.impact(style: .medium) // selection Feedbacker.selection()おわり
間違えてる部分や編集リクエストありましたらよろしくお願いいたします
- 投稿日:2019-04-05T11:05:51+09:00
Swift で RTMP Handshake を実装する
RTMPとは
RTMPは、Adobe が開発している Adobe Flash プレーヤーとサーバーの間で、音声・動画・データをやりとりするストリーミングのプロトコルです。
RTMP の主要な利用法は Flash Video を再生することでしたが、低遅延でのストリーミングを実現できることから、ストリーミングサーバーへの映像伝送に使われたりもします。ネットで調べると大体のLive配信アプリはRTMPで配信し、HLSで視聴しているものが多いようです。
RTMP Handshakeとは
RTMPはTCP上で動きます。
TCPによる接続が確立すると、RTMPの接続をハンドシェイクから行っていきます。
RTMPのハンドシェイクは他のプロトコルとは異なります。
クライアント(接続を開始したエンドポイント)とサーバーはそれぞれ3つの固定長のチャンクを送信します。
これらのチャンクは、クライアントから送信されるものはC0、C1、C2、サーバーから送信されるものは0、S1、S2と呼ばれています。ハンドシェイクは、クライアントがC0およびC1チャンクを送信することから始まります。
クライアントは、C2を送信する前にS1が受信されるまで待つ必要があります。
また、その他のデータ(音声や映像など)を送信するためにはS2の受信を待つ必要があります。
逆にサーバーはS0、S1を送る前にC0が受け取られる必要があります。(C1の受信まで待機してもOK)
S2を送信するにはC1が受信されるまで待つ必要があります。
そしてクライアントと同様その他のデータを送信するにはC2の受信を待つ必要があります。
図にするとこんな感じです。しかし、C0とかS0とか言われても正直良くわかりません。
実際の通信を見ると、なるほど、ほんとにC0とかが送られているんだなとわかります。ほんとにそういう名前なんですねー。
これらのチャンクには決まったフォーマットがありますが、フォーマットに関する説明は 仕様書 の「5.2.2. C0 and S0 Format」「5.2.3. C1 and S1 Format」「5.2.4. C2 and S2 Format」に詳しく書いてあるので割愛します。
こちらの記事も仕様書の日本語版くらいわかりやすかったので参考にしてみてください。
https://developers.cyberagent.co.jp/blog/archives/13739/#handshakeこれも実際の通信を見てみると当たり前ですがフォーマット通りのチャンクが送受信されていることがわかります。
ハンドシェークの実装
では実装していきます。
ソケット接続自体は NSStream を使えば簡単です。let inputQueue = DispatchQueue(label: "NetSocket.input") let outputQueue = DispatchQueue(label: "NetSocket.output") var inputStream: InputStream? var outputStream: OutputStream? func open(hostname: String, port: Int) { Stream.getStreamsToHost( withName: hostname, port: port, inputStream: &self.inputStream, outputStream: &self.outputStream ) inputQueue.async { self.inputStream?.delegate = self self.inputStream?.schedule(in: .main, forMode: .default) self.outputStream?.delegate = self self.outputStream?.schedule(in: .main, forMode: .default) self.inputStream?.open() self.outputStream?.open() } }読み込みと書き込みについてはそれぞれ
InputStream#read
とOutputStream#write
を使って以下のように書けます。func read() { outputQueue.async { let maxLength = Int(UInt16.max) let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxLength) let count = self.inputStream?.read(buffer, maxLength: maxLength) ?? 0 var data = Data() if 0 < count { data.append(buffer, count: count) self.listen(data) // listen(_:)ついては後述 } } } func write(data: Data) { DispatchQueue.main.async { let length = data.count var leftLength = length data.withUnsafeBytes { (buffer: UnsafePointer<UInt8>) -> Void in while leftLength > 0 { let count = self.outputStream?.write(buffer + (length - leftLength), maxLength: length) ?? 0 leftLength -= count } } } }Input/OutputのStreamの状態が変化すると
StreamDelegate#stream(Stream, handle: Stream.Event)
が呼ばれます。
これで通信可能になったとき(.openCompleted
)と何かを受信したとき(.hasBytesAvailable
)がわかるのでここからチャンクの送受信をおこなっていきます。ソケットをオープンして通信が可能になったらクライアントからC0とC1を送信します。
C1まで送信するとサーバーからS0、S1、S2が送られてくるので先程のread()
メソッドを使って受信したチャンクを読みます。func stream(_ aStream: Stream, handle eventCode: Stream.Event) { switch eventCode { case .openCompleted: guard inputStream?.streamStatus == .open, outputStream?.streamStatus == .open else { break } if aStream == inputStream { sendC0C1packet() } case .hasBytesAvailable: if aStream == inputStream { read() } case .errorOccurred: print("errorOccurred") default: break } }まずはC0とC1の送信です。
※整数のバイト配列への変換については HaishinKit.swift の ExpressibleByIntegerLiteral+Extension.swift を使わせてもらってます。func sendC0C1packet() { var data = Data() // C0 let protocolVersion: UInt8 = 3 data.append(protocolVersion.data) // C1 let time = UInt32(0) data.append(time.bigEndian.data) let zero = Data([0x00, 0x00, 0x00, 0x00]) data.append(zero) let size = 1536 - 8 // C1の固定長 - time+zero var random = Data(count: size) var _ = random.withUnsafeMutableBytes { bytes in SecRandomCopyBytes(kSecRandomDefault, size, bytes) } data.append(random) write(data: data) handshakeStatus = .c0c1 }次にS0、S1、S2の読み取りです。
先程read()
メソッドの中に登場したlisten(_:)
を実装していきます。
※ HaishinKit.swift を参考にしています。ハンドシェイクのステータスをenumで管理しています。
先程のsendC0C1packet()
を実行した時点でステータスは.c0c1
になっています。
ハンドシェイクは上述の通りルールが決まっているので、C0とC1を送ったならサーバからはS0、S1が来るはずです。
というわけで、.c0c1
の状態でサーバーからチャンクを受け取ったらそれはS0、S1だろうということで次はC2を送ります(ステータスは.c2
)。
.c2
の状態でサーバーからチャンクを受け取ったらそれはS1だろうということでこれでハンドシェイクは完了です。enum HandshakeStatus { case none case c0c1 case c2 case done } func listen(_ data: Data) { switch handshakeStatus { case .none: break case .c0c1: if data.count <= 1536 { break } sendC2packet(data) handshakeStatus = .c2 var data = data data.removeSubrange(0... 1536) // S0、S1、S2はいっぺんに送られてくることもあるのでS0、S1を取り除いてS2を渡して再起実行します if 1536 <= data.count { listen(data) } case .c2: handshakeStatus = .done // ハンドシェイク完了 case .done: break } }func sendC2packet(_ s0s1packet: Data) { var data = Data() let time = s0s1packet.subdata(in: 1..<5) data.append(time) let time2 = UInt32(Date().timeIntervalSince1970) data.append(time2.bigEndian.data) let random = s0s1packet.subdata(in: 9..<1536+1) data.append(random) write(data: data) handshakeStatus = .c2 }まとめ
RTMP配信の序章を実装してみましたが、ハンドシェイクの仕様を理解するのが一番のハードルでした。
通信処理自体はNSStream
を使えば簡単に書けてしまうので実装自体はそう難しくなかったと思います。
ハンドシェイクの段階ではまだ受信したデータのパース処理が無いので(比較的)楽ですね。
しかしハンドシェイクが終わっても、このあとクライアントからconnectコマンドメッセージを送信してサーバーからresultを受信して今度はクライアントから...みたいな感じで音声や映像を送信するまではまだ道が長いです。。
自前で実装するのはかなりめんどくさいですが、仕様書や実際の通信をキャプチャして仕様を理解していくのは面白いなと思いました。参考
- Real-Time Messaging Protocol (RTMP) specification (Version 1.0) http://wwwimages.adobe.com/www.adobe.com/content/dam/acom/en/devnet/rtmp/pdf/rtmp_specification_1.0.pdf
- RTMP 1.0 準拠のサーバーをGo言語で実装する https://developers.cyberagent.co.jp/blog/archives/13739/#c0s0format
- shogo4405/HaishinKit.swift https://github.com/shogo4405/HaishinKit.swift