はじめに
個人的に開発を続けているこちらのアプリの機能として動画つきのツイートをtwitterに投稿できるようにしたいと思い、いろいろ試行錯誤しましたが、思いのほか手こずったので書き残します。
今回はSocial Framework と Accounts Framework を使って実装していきますが、
動画のアップロードに関することを主題として扱うため、アカウントの取得方法やSocial Frameworkの詳しい使い方などは省略しています。
Twitter APIについて
Swiftのコードを見る前に、まずはじめに私がつまずいたTwitterのAPIの仕様について見てみましょう。
エンドポイント
今回は動画のアップロード用のエンドポイントとツイートをポストするエンドポイントの2つを使います。
https://upload.twitter.com/1.1/media/upload.json
https://api.twitter.com/1.1/statuses/update.json
APIリクエストの手順
Twitterでは、APIから動画のアップロードを行うのに合計3回のAPIリクエストを投げることになります。
(これを知らずに画像のアップロードと同じようにAPIを叩いたりしていきなりつまずきました。。)
ざっくり説明していきますが、この辺りは以下を参考にしていただいた方がわかりやすいと思います。
https://dev.twitter.com/rest/public/uploading-media#chunkedupload
https://syncer.jp/twitter-api-matome/post/media/upload_chunked
また、3度のリクエストで動画をアップロードした後、さらにその結果をもってツイートをポストするので、合わせて4回のAPIリクエストを行います。
では、3度のリクエストの流れをざっくりと見ていきましょう。
INIT リクエスト
動画アップロードの1度目のリクエストでは、「こういうファイルを送りますよ」という準備的なリクエストを送ります。
パラメータは以下
- command
- media_type(例:
video/mp4
) - total_bytes(例:
4430752
)
commandパラメータは3回のリクエスト全てに出てきますが、何度目のリクエスト(どの工程のリクエスト)かを判別するものになります。
1度目のリクエストの場合はcommandの値はINITになります。
送信されるパラメータ例:
"command=INIT&media_type=video/mp4&total_bytes=4430752"
成功すると以下のようなレスポンスが返ってきます。
{
"media_id": 601413451156586496,
"media_id_string": "601413451156586496",
"expires_after_secs": 3599
}
APPEND リクエスト
2度目のリクエストでは実際に動画ファイルをアップロードするリクエストになります。
パラメータは以下
- command
- media_id
- segment_index
media_id
はINITリクエストのレスポンスとして得られた値を使用します。
また、上記パラメータに加えて、実際にアップロードするファイルをつけます。
パラメータの詳細は前述の参考URLをご参照ください。笑
送信されるパラメータ例:
"command=APPEND&media_id=601413451156586496&segment_index=0"
このリクエストは、成功しても特にレスポンスデータはありません。
200番台のステータスコードが返ってきたら成功と判断します。
FINALIZE リクエスト
最後のリクエストです。
このリクエストでアップロードした動画が有効になります。
パラメータは以下
- command
- media_id
こちらのmedia_id
もAPPENDリクエストの時と同様にINITリクエストのレスポンスで得られた値を使用します。
こちらは成功すると以下のようなレスポンスが返ってきます。
ようやくアップロードした動画をつけてツイートする準備が整いました。
{
"media_id": 601413451156586496,
"media_id_string": "601413451156586496",
"size": 4430752,
"expires_after_secs": 3600,
"video": {
"video_type": "video/mp4"
}
}
動画をつけてツイートする
前述の工程で得られたmedia_id
をツイートをポストする際のmedia_ids
パラメータに設定した上でAPIを叩けばOKです。
Swiftで書いてみる
前述の説明ではかなり端折った部分が多いので、実際にコードを見た方がわかるかもしれません。
※冒頭で記述したアプリ内で使う予定のコードを改変したものなので、若干適当な部分がありますがご容赦ください。
また、自分が使う都合なので、今回のコードはfileManagerでのファイルパスからの動画ファイル取得に限っています。
import UIKit
import Social
import Accounts
import SwiftyJSON
struct Twitter {
var account: ACAccount
let fileManager = NSFileManager.defaultManager()
init(account: ACAccount) {
self.account = account
}
let uploadURL = NSURL(string: "https://upload.twitter.com/1.1/media/upload.json")
let statusURL = NSURL(string: "https://api.twitter.com/1.1/statuses/update.json")
func postWithMovie(tweet: String, filePath: String, success: (responseData: NSData!, urlResponse: NSHTTPURLResponse!) -> Void, failure: ((error: NSError!) -> Void)?) {
guard let mediaData = NSData(contentsOfFile: filePath) else {
return
}
do {
let fileAttr = try fileManager.attributesOfItemAtPath(filePath)
if let fileSize = fileAttr[NSFileSize] as? Int {
postMedia(tweet, mediaData: mediaData, fileSize: String(fileSize), success: success, failure: failure)
}
} catch {
return
}
}
// このメソッドが前述までの3回のリクエスト+動画付きツイートを行う部分
private func postMedia(tweet: String, mediaData: NSData, fileSize: String, success: (responseData: NSData!, urlResponse: NSHTTPURLResponse!) -> Void, failure: ((error: NSError!) -> Void)?) {
// INIT リクエスト
uploadVideoInitRequest(fileSize, success: { (responseData) -> () in
let json = JSON(data: responseData)
// レスポンスから media_id_string を取得して APPEND リクエストに利用
let mediaIdString = json["media_id_string"].stringValue
// APPEND リクエスト
self.uploadVideoAppendRequest(mediaData, mediaIdString: mediaIdString, success: { () -> () in
// FINALIZE リクエスト
self.uploadVideoFinalizeRequest(mediaIdString, success: { (responseData) -> () in
let statusKey: NSString = "status"
let mediaIDKey: NSString = "media_ids"
let statusRequest = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .POST, URL: self.statusURL, parameters: [statusKey : tweet, mediaIDKey : mediaIdString])
statusRequest.account = self.account
// 動画をつけてツイート
statusRequest.performRequestWithHandler { (responseData: NSData!, urlResponse: NSHTTPURLResponse!, error: NSError!) -> Void in
if let error = error {
failure?(error: error)
}
// 成功したらなんかする
success(responseData: responseData, urlResponse: urlResponse)
}
}, failure: { (error) -> () in
failure?(error: error)
})
}, failure: { (error) -> () in
failure?(error: error)
})
}) { (error) -> () in
failure?(error: error)
}
}
// INIT リクエスト
private func uploadVideoInitRequest(fileSize: String, success: (responseData: NSData) -> (), failure: ((error: NSError) -> ())?) {
let commandKey: NSString = "command"
let mediaTypeKey: NSString = "media_type"
let totalBytesKey: NSString = "total_bytes"
let initParams: [NSString: AnyObject] = [commandKey: "INIT", mediaTypeKey: "video/mp4", totalBytesKey: fileSize]
let initRequest = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .POST, URL: self.uploadURL, parameters: initParams)
initRequest.account = account
initRequest.performRequestWithHandler { (responseData: NSData!, urlResponse: NSHTTPURLResponse!, error: NSError!) -> Void in
if let error = error {
failure?(error: error)
return
}
success(responseData: responseData)
}
}
// APPEND リクエスト
private func uploadVideoAppendRequest(mediaData: NSData, mediaIdString: String, success: () -> (), failure: ((error: NSError) -> ())?) {
let commandKey: NSString = "command"
let mediaIdKey: NSString = "media_id"
let segmentIndexKey: NSString = "segment_index"
let appendParam: [NSString: AnyObject] = [commandKey: "APPEND", mediaIdKey: mediaIdString, segmentIndexKey: "0"]
let appendRequest = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .POST, URL: self.uploadURL, parameters: appendParam)
appendRequest.addMultipartData(mediaData, withName: "media", type: "video/mp4", filename: nil)
appendRequest.account = account
appendRequest.performRequestWithHandler { (responseData: NSData!, urlResponse: NSHTTPURLResponse!, error: NSError!) -> Void in
if let error = error {
failure?(error: error)
return
}
if urlResponse.statusCode < 300 && urlResponse.statusCode >= 200 {
success()
}
}
}
// FINALIZE リクエスト
private func uploadVideoFinalizeRequest(mediaIdString: String, success: (responseData: NSData) -> (), failure: ((error: NSError) -> ())?) {
let commandKey: NSString = "command"
let mediaIdKey: NSString = "media_id"
let finalizeParam: [NSString: AnyObject] = [commandKey: "FINALIZE", mediaIdKey: mediaIdString]
let finalizeRequest = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .POST, URL: self.uploadURL, parameters: finalizeParam)
finalizeRequest.account = account
finalizeRequest.performRequestWithHandler { (responseData: NSData!, urlResponse: NSHTTPURLResponse!, error: NSError!) -> Void in
if let error = error {
failure?(error: error)
return
}
success(responseData: responseData)
}
}
}
これで以下のように使えます。
let twitter = Twitter(account: account) // accountは 事前に取得したACAccount
twitter.postWithMovie("ツイートの文章",
filePath: "動画ファイルのパス",
success: { (responseData, urlResponse) -> Void in
// 成功した時になんかする
// 「ツイートしました」ってアラート出したり?
}) { (error) -> Void in
// エラー時になんかする
}
おわりに
Social Frameworkでさらっとできるかと思っていたのですが、思いの外苦戦してしまいました。
なんだかすごく面倒ですね、、知らないだけでもっと簡単な方法があるんでしょうか。。
ご存知の方いらっしゃったら教えてください。