Amazon Elastic TranscoderとはAmazon Web Serviceの一つ(ベータ版)で
メディア変換をできるクラウドサービス。
あんまりメディアファイルを扱ったこと無いから利用イメージが湧かないが、
iOS用のストリーミング技術のHLSへの変換が簡単そうなので触ってみることにした。
基本的な使い方は安定のクラスメソッドさんのブログで↓
http://dev.classmethod.jp/cloud/amazon-elastic-transcoder-start/
http://dev.classmethod.jp/references/amazon-elastic-transcoder-hls/
一般的なWeb言語のSDKは存在するものの、apex用のSDKはもちろん無いので
pipeline(入出力バケットの設定)とjob(実際の変換操作)の作成部分を作ってみた。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 |
/** * AETコモンクラス */ public with sharing class AETCommon { private final String SERVICE = 'elastictranscoder'; /** * Access Key Id */ private String AWSAccessKeyId = EnvSetting__c.getOrgDefaults().AWSAccessKeyID__c; /** * Access Key Secret */ private String AWSAccessKeySecret = EnvSetting__c.getOrgDefaults().AWSAccessKeySecret__c; /** * パイプラインを作成 */ public String createPipeLine( String region, String pipelineName, String inputBucket, String outputBucket, String thumnailBucket, String role ) { Map<String, String> params = new Map<String, String>{ }; List<String> signedHeaders = new List<String>{ 'host', 'x-amz-date' }; DateTime dt = DateTime.now(); String credentialScope = dt.formatGmt('YYYYMMdd') + '/' + region + '/' + SERVICE + '/aws4_request'; //Body Map<String, Object> jsonMap = new Map<String, Object>{ 'Name' => pipelineName, 'InputBucket' => inputBucket, 'OutputBucket' => outputBucket, 'Role' => role, 'Notifications' => new Map<String, Object>{ 'Progressing' => '', 'Completed' => '', 'Warning' => '', 'Error' => '' } }; String jsonBody = Json.serialize(jsonMap); String resource = '/2012-09-25/pipelines'; String stringToSign = this.createStringToSign( 'AWS4-HMAC-SHA256', dt, credentialScope, this.createHashedCanonicalRequest( 'SHA256', 'POST', resource, params, new Map<String, String>{ 'Host' => 'elastictranscoder.' + region + '.amazonaws.com', 'x-amz-date' => dt.formatGmt('YYYYMMdd') + 'T' + dt.formatGmt('HHmmss') + 'Z' }, signedHeaders, jsonBody ) ); String signature = this.createSignature( 'hmacSHA256', dt, region, SERVICE, Blob.valueOf(stringToSign) ); return this.callCommonAETCall( dt, 'elastictranscoder.' + region + '.amazonaws.com', credentialScope, signedHeaders, signature, resource, jsonBody ); } /** * jobを作成 */ public String createJob( String region, String pipelineId, String inputKey, String outputKey, String outputKeyPrefix ) { Map<String, String> params = new Map<String, String>{ }; List<String> signedHeaders = new List<String>{ 'host', 'x-amz-date' }; DateTime dt = DateTime.now(); String credentialScope = dt.formatGmt('YYYYMMdd') + '/' + region + '/' + SERVICE + '/aws4_request'; //Body Map<String, Object> jsonMap = new Map<String, Object>{ 'PipelineId' => pipelineId, 'Input' => new Map<String, Object>{ 'Key' => inputKey }, 'Outputs' => new List<Map<String,Object>>{ new Map<String, Object>{ 'Key' => outputKey, 'PresetId' => '1351620000001-200050', 'SegmentDuration' => '5' } }, 'OutputKeyPrefix' => outputKeyPrefix }; String jsonBody = Json.serialize(jsonMap); String stringToSign = this.createStringToSign( 'AWS4-HMAC-SHA256', dt, credentialScope, this.createHashedCanonicalRequest( 'SHA256', 'POST', '/2012-09-25/jobs', params, new Map<String, String>{ 'Host' => 'elastictranscoder.' + region + '.amazonaws.com', 'x-amz-date' => dt.formatGmt('YYYYMMdd') + 'T' + dt.formatGmt('HHmmss') + 'Z' }, signedHeaders, jsonBody ) ); String signature = this.createSignature( 'hmacSHA256', dt, region, SERVICE, Blob.valueOf(stringToSign) ); return this.callCommonAETCall( dt, 'elastictranscoder.' + region + '.amazonaws.com', credentialScope, signedHeaders, signature, '/2012-09-25/jobs', jsonBody ); } /** * AETへのAPIコールの共通メソッド */ private String callCommonAETCall( DateTime dt, String hostname, String credentialScope, List<String> signedHeaders, String signature, String resource, String jsonBody ) { HttpRequest req = new HttpRequest(); req.setHeader('Host', hostname); req.setHeader( 'x-amz-date', dt.formatGmt('YYYYMMdd') + 'T' + dt.formatGmt('HHmmss') + 'Z' ); req.setHeader('Content-Type', 'application/json; charset=UTF-8'); req.setHeader('Content-Length', String.valueOf(jsonBody.length())); req.setHeader( 'Authorization', 'AWS4-HMAC-SHA256 Credential=' + this.AWSAccessKeyId + '/' + credentialScope + ',' + 'SignedHeaders=' + String.join(Utility.getLowerCaseSortedList(signedHeaders), ';') + ',' + 'Signature=' + signature ); req.setEndpoint('https://' + hostname + resource); req.setMethod('POST'); req.setBody(jsonBody); Http http = new Http(); HTTPResponse res = http.send(req); return res.getBody(); } /** * HashedCanonicalRequestを作成 */ private String createHashedCanonicalRequest( String algorithm, String method, String url, Map<String, String> params, Map<String, String> headers, List<String> signedHeaders, String payload ) { String signature = ''; signature += method + Constants.LF; signature += url + Constants.LF; signature += Utility.getSortedParam(params) + Constants.LF; signature += Utility.createCanonicalHeaders(headers) + Constants.LF; signature += String.join(Utility.getLowerCaseSortedList(signedHeaders), ';').toLowerCase() + Constants.LF; signature += EncodingUtil.convertToHex( Crypto.generateDigest( algorithm, Blob.valueOf(payload) ) ); return EncodingUtil.convertToHex( Crypto.generateDigest( algorithm, Blob.valueOf(signature) ) ).toLowerCase(); } /** * StringToSignを作成 */ private String createStringToSign( String algorithm, DateTime requestDateTime, String credentialScope, String hashedCanonicalRequest ) { String stringToSign = algorithm + Constants.LF; stringToSign += requestDateTime.formatGmt('YYYYMMdd') + 'T' + requestDateTime.formatGmt('HHmmss') + 'Z' + Constants.LF; stringToSign += credentialScope + Constants.LF; stringToSign += hashedCanonicalRequest; return stringToSign; } private String createSignature( String algorithm, DateTime requestDateTime, String region, String service, Blob stringToSign ) { system.debug(stringToSign.toString()); Blob kDate = Crypto.generateMac( algorithm, Blob.valueOf(requestDateTime.formatGmt('YYYYMMdd')), Blob.valueOf('AWS4' + this.AWSAccessKeySecret) ); Blob kRegion = Crypto.generateMac( algorithm, Blob.valueOf(region), kDate ); Blob kService = Crypto.generateMac( algorithm, Blob.valueOf(service), kRegion ); Blob kSigning = Crypto.generateMac( algorithm, Blob.valueOf('aws4_request'), kService ); return EncodingUtil.convertToHex( Crypto.generateMac( algorithm, stringToSign, kSigning ) ); } } /** * Utilityクラス */ public without sharing class Utility { /** * key=value&...でつないだ文字列を返却 * @param mapParam 対象文字列=>文字列マップ * @return key=value&...でつないだ文字列 */ public static String getSortedParam(Map<String,String> mapParam){ if (mapParam == null || mapParam.keySet().isEmpty()) { return ''; } String param = ''; List sortedKey = getSortedList(mapParam.keySet()); for(String key: sortedKey){ param += percentEncodeRfc3986(key) + '=' + percentEncodeRfc3986(mapParam.get(key)) + '&'; } return param.substring(0,param.length() - 1); } public static String percentEncodeRfc3986(String s) { return EncodingUtil.urlEncode(s, 'UTF-8') .replace('+', '%20') .replace('*', '%2A') .replace('%7E', '~'); } public static String createCanonicalHeaders(Map<String, String> headers) { if (headers == null || headers.keySet().isEmpty()) { return ''; } Map<String, String> lowerCaseHeaders = new Map<String, String>(); for (String key : headers.keySet()) { lowerCaseHeaders.put(key.toLowerCase(), headers.get(key)); } String param = ''; List sortedKey = getSortedList(lowerCaseHeaders.keySet()); for(String key: sortedKey){ param += key + ':' + lowerCaseHeaders.get(key).trim() + '\n'; } return param; } public static List getLowerCaseSortedList(List keys) { List sortedKeys = new List(); for (String key : keys) { sortedKeys.add(key.toLowerCase()); } sortedKeys.sort(); return sortedKeys; } } |
Signature Version 4っていう署名生成規則でリクエストしないといけないんだけど、見事にハマった…。
1. HashedCanonicalRequestを作成(上記サンプルのcreateHashedCanonicalRequest)
2. 1を使ってStringToSignを作成(上記サンプルのcreateStringToSign)
3. StringToSign、SecretKey、その他パラメータからSignature生成(上記サンプルのcreateSignature)
っていう流れで、Signatureが間違っている場合(The request signature we calculated does not match the signature you provided.って言われる )は
正解のHashedCanonicalRequestとStringToSignをエラーメッセージと同時に教えてくれるので、
これを参考にプログラム側を修正していけば、いずれ成功します。
ちなみにapexでのSignature Version4に関しては
テラスカイさんのブログ(http://www.terrasky.co.jp/blog/?p=3280)でも紹介されてるので、
そちらも参考すると良い感じ。
上記サンプルはmp4とかの形式の動画ファイルをHLS形式(tsファイル&m3u8ファイル)にするもので
「salesforceから動画をアップロードしてiOSでストリーミング配信したい!」
とか言う要件があった場合に使えそう。
その場合はapexやJSからS3にアップロード→パイプラインとジョブ作成 って感じかな。
ジョブは非同期処理なので、SNSとかとうまく組み合わせて
完了通知や完了後処理を行うと良さそう。
完了通知だとEメール、完了後処理となるとHTTP通知とかかな。
0 Comments
1 Pingback