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(実際の変換操作)の作成部分を作ってみた。
/**
* 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っていう署名生成規則でリクエストしないといけないんだけど、見事にハマった…。
HashedCanonicalRequestを作成(上記サンプルのcreateHashedCanonicalRequest)
1を使ってStringToSignを作成(上記サンプルのcreateStringToSign)
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通知とかかな。