事前準備
以下6点の情報をブライトコーブ管理者に相談して、
事前に準備しておいてください
- ブライトコーブ アカウントID
- ブライトコーブ クライアントID
- ブライトコーブ 秘密鍵
- ブライトコーブ API 権限設定
- ブライトコーブ テスト用の動画を用意する(動画IDを用意する)
- 公開サーバー上にテスト用の画像を置く
アカウントIDは、以下のURLに確認手順が書いてあります
(ブライトコーブを使ったことがあれば、アカウントIDは周知だと思いますが念のため)
クライアントIDと秘密鍵の発行手順は、以下のURLに詳しく書いてあります
APIの権限設定は、ブライトコーブ管理画面 左下 歯車アイコン(admin) > API認証(API Authentication) から行えます。
※ブライトコーブ管理者アカウントのみ表示されます
- Exposed Brightcove APIs
- CMS - Video Read/Write
- Dynamic Ingest - Create
- Ingestion Profiles - Configuration Read
- Ingestion Profiles - Read
どこかで拾ったパーミッション設定の画像を載せておきます。
ここまで出来れば、以下のソースコードを使うことで
画像の差し替えが可能になります。
実装
以下のソースコードを、PHPが動くサーバー上に「 bc_image/upload.php
」など、
任意の名前をつけて置いてからGETメソッドでアクセスしてください。
定数の値は適宜、変更してください。
<?php
/**
* [NOTE]
* 画像アップロードの前に、必要な権限をブライトコーブ上で許可しておくこと
* ブライトコーブ管理画面 左下 歯車アイコン(admin) > API認証(API Authentication)
* Exposed Brightcove APIs
* - CMS - Video Read/Write
* - Dynamic Ingest - Create
* - Ingestion Profiles - Configuration Read
* - Ingestion Profiles - Read
*/
ini_set('display_errors', 1);
define("BC_ACCOUNT_ID", "1234567890123"); // 1. ブライトコーブのアカウントIDを入力する
define("BC_CLIENT_ID", "your_brightcove_client_id_here"); // 2. ブライトコーブのクライアントIDを入力する
define("BC_CLIENT_SECRET", "your_brightcove_secret_key_here"); // 3. ブライトコーブの秘密鍵を入力する
define("BC_TEST_VIDEO_ID", "1234567890123"); // 5. ブライトコーブ テスト用動画IDを入力する
define("BC_TEST_IMAGE_URL", "https://www.example.com/images/test.jpg"); // 6. 公開サーバー上に置いた画像のURLを入力する
/**
* 画像のサイズ情報を返す
*/
function getImageInfo($imageUrl)
{
$info = getimagesize($imageUrl);
$ret = [
"width" => $info[0],
"height" => $info[1],
"mime" => $info["mime"]
];
return $ret; // array or false
}
/**
* ブライトコーブの画像更新に必要なフォーマットを返す
*
* [NOTE] 公式リファレンスにはurlだけが必須パラメータである旨記載されていたが、
* 実際にはwidthとheightも入力しなければ更新されなかった
* @see https://ja.apis.support.brightcove.com/dynamic-ingest/references/reference.html#tag/Ingest/operation/AccountsVideosIngestRequestsByAccountIdAndVideoIdPost
*/
function getBcImageFormat($imageUrl)
{
$imageInfo = getImageInfo($imageUrl);
$imageFormat = [
"poster" => [
"url" => $imageUrl,
"width" => $imageInfo["width"],
"height" => $imageInfo["height"]
],
"thumbnail" => [
"url" => $imageUrl,
"width" => $imageInfo["width"],
"height" => $imageInfo["height"]
],
"images" => [
[
"variant" => "poster",
"url" => $imageUrl,
"width" => $imageInfo["width"],
"height" => $imageInfo["height"]
],
[
"variant" => "thumbnail",
"url" => $imageUrl,
"width" => $imageInfo["width"],
"height" => $imageInfo["height"]
]
]
];
return $imageFormat;
}
function updateBcImages($videoId, $imageFormat)
{
$response = postData($videoId, $imageFormat);
return $response;
}
/**
* 画像をブライトコーブサーバーに反映する
*/
function postData($videoId, $data)
{
$accountId = BC_ACCOUNT_ID;
$apiUrlBase = "https://ingest.api.brightcove.com/v1/";
$apiPath = "accounts/{$accountId}/videos/{$videoId}/ingest-requests";
$url = $apiUrlBase . $apiPath;
$accessToken = getBcToken();
//send the http request
$options = [
CURLOPT_POST => TRUE,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => TRUE,
CURLOPT_SSL_VERIFYPEER => FALSE,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => array(
'Content-type: application/json',
"Authorization: Bearer {$accessToken}",
)
];
$ch = curl_init($url);
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
curl_close($ch);
// Check for errors
if ($response === FALSE) {
echo '{"ERROR": "There was a problem with your API call"}';
die(curl_error($ch));
}
// return the response to the AJAX caller
$response = json_decode($response);
if(! isset($response)){
echo '{null}';
die(curl_error($ch));
}
return $response;
}
/**
* ブライトコーブのアクセストークンを取得する
*/
function getBcToken()
{
$data = array();
$clientId = BC_CLIENT_ID;
$clientSecret = BC_CLIENT_SECRET;
$authString = "{$clientId}:{$clientSecret}";
$apiUrlBase = "https://oauth.brightcove.com/v4/";
$apiPath = "access_token?grant_type=client_credentials";
$url = $apiUrlBase . $apiPath;
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_POST => TRUE,
CURLOPT_RETURNTRANSFER => TRUE,
CURLOPT_SSL_VERIFYPEER => FALSE,
CURLOPT_USERPWD => $authString,
CURLOPT_HTTPHEADER => array('Content-type: application/x-www-form-urlencoded'),
CURLOPT_POSTFIELDS => $data
));
$response = curl_exec($ch);
curl_close($ch);
// Check for errors
if ($response === FALSE) {
echo '{"ERROR": "There was a problem with your API call"}';
die(curl_error($ch));
}
// return the response to the AJAX caller
$response = json_decode($response);
if(! isset($response)){
echo '{null}';
die(curl_error($ch));
}
$accessToken = $response->access_token;
return $accessToken;
}
function main()
{
$videoId = BC_TEST_VIDEO_ID; // ブライトコーブの動画ID
$imageUrl = BC_TEST_IMAGE_URL; // 差し替える画像のURL
$imageFormat = getBcImageFormat($imageUrl);
// Update Brightcove Images
$job = updateBcImages($videoId, $imageFormat);
$message = "";
$message .= '画像アップロードに成功しました。ジョブID: ' . $job->id . "<br>";
$message .= '次の画像を反映中です。' . '<a href="' . $imageUrl. '" target="_blank">' . $imageUrl . '</a>';
echo $message;
exit;
}
main();
動作確認
つまづきポイント
画像の差し替えにはDynamic Ingest APIを使用する
まずこの結論にたどり着くのに時間がかかりました。
ブライトコーブAPIの情報なさすぎワロタ。結構需要がありそうなものですが。。
画像更新を行うには、アクセストークンが必要である
これは過去に別のAPIで開発実績があったのでそこまでつまづきませんでしたが、
アクセストークンが必要であることを念押ししておきます。
アクセストークン取得のサンプルコードは以下のURLを参考にしてください
画像更新には、画像の横幅と縦幅の情報が事実上、必須である
公式リファレンスには、画像更新に必須のパラメータはurlのみである、と記載がありましたが、
実際にはwidthとheightの値も必須でした。
これを省略すると、送信は成功しますがどれだけ待っても反映されませんでした。
非公開サーバーに置いた画像は取得できないので差し替え不可
当たり前ではありますが、ブライトコーブAPIからURL経由で画像を取得しますので、画像が非公開のS3バケットなどにある場合、当然ながら画像の取得が出来ず、504 Gateway Timeoutになります。
公開中のS3バケットやWEBサーバーを用意するなどして、そこに画像を置くようにしてください。
セキュアにしたいならば、スクリプト自体はIP制限を設けたWEBサーバーに置きつつ、画像アップロードをs3::putObjectで公開済のS3バケットに置いて、s3::getObjectでURLを取得する、といった感じでしょうか。誰かサンプル書いてくれや
参考URL
「Ingest Videos and Assets」の項目を参照してください
追記 Javascript版の実装
実際には使いませんでしたが、APIの検証目的でjavascriptで作ったコードがあったので供養のために置いておきます。
これでも画像の差し替えが出来ることは確認してあります。
/**
* グローバル変数
*/
var API_PATH_BASE = "./bc-proxy/";
var BC_ACCOUNT_ID = "1234567890123";
var BC_TOKEN = "";
var BC_API_URL_BASE = "https://cms.api.brightcove.com/v1/";
var BC_INGEST_API_URL_BASE = "https://ingest.api.brightcove.com/v1/";
var EXPIRED_TIME = 0;
/**
* Get Data By Ajax
*
* @param string filePath
* @return Deferred Object
*/
function getData(filePath)
{
var def = new $.Deferred;
var options = {
type : 'GET',
url : filePath,
dataType : 'json',
cache : false
};
$.ajax(options)
.done(function(data, textStatus, jqXHR){
console.log('getData done', data);
def.resolve(data);
})
.fail(function(jqXHR, textStatus, errorThrown){
console.error('getData fail', jqXHR);
def.reject(jqXHR);
});
return def.promise();
}
/**
* Post Data By Ajax
*
* @param string filePath
* @param object postParams
* @return Deferred Object
*/
function postData(filePath, inputs)
{
var def = new $.Deferred;
//var csrfToken = this.getCsrfToken();
//postParams = Object.assign(postParams, csrfToken);
var options = {
type : 'POST',
url : filePath,
data : inputs,
dataType : 'json',
cache : false
};
$.ajax(options)
.done(function(data, textStatus, jqXHR){
console.log('postData done', data);
def.resolve(data);
})
.fail(function(jqXHR, textStatus, errorThrown){
console.error('postData fail', errorThrown);
def.reject(errorThrown);
});
return def.promise();
}
/**
* Put Data By Ajax
*
* @param string filePath
* @return Deferred Object
*/
function putData(filePath, inputs)
{
var def = new $.Deferred;
var options = {
type : 'PUT',
url : filePath,
data : inputs,
dataType : 'json',
cache : false
};
$.ajax(options)
.done(function(data, textStatus, jqXHR){
console.log('putData done', data);
def.resolve(data);
})
.fail(function(jqXHR, textStatus, errorThrown){
console.error('putData fail', errorThrown);
def.reject(errorThrown);
});
return def.promise();
}
/**
* Post Data By Ajax
*
* @param string filePath
* @param object inputs
* @return Deferred Object
*/
function patchData(filePath, inputs)
{
var def = new $.Deferred;
//var csrfToken = this.getCsrfToken();
//postParams = Object.assign(postParams, csrfToken);
var options = {
type : 'PATCH',
url : filePath,
data : inputs,
dataType : 'json',
cache : false
};
$.ajax(options)
.done(function(data, textStatus, jqXHR){
console.log('patchData done', data);
def.resolve(data);
})
.fail(function(jqXHR, textStatus, errorThrown){
console.error('patchData fail', errorThrown);
def.reject(errorThrown);
});
return def.promise();
}
/**
* クエリパラメータをJSON形式で取得する
*
* @doc https://medialize.github.io/URI.js/
* @doc https://thebaker.hatenablog.com/entry/2018/02/08/154315
*/
function getQueries()
{
var uri = new URI();
var queries = uri.query(true);
return queries;
}
/**
* 特定のクエリパラメータの値を取得する
*
* @param string クエリパラメータ名 e.g. "team_id"
* @return mixed クエリパラメータの値 e.g. "7"
*/
function getQuery(key)
{
var queries = getQueries();
var value = queries[key];
return value;
}
/**
* クエリパラメータのオブジェクトからクエリ文字列を生成する
*
* @doc https://medialize.github.io/URI.js/docs.html#static-buildQuery
*/
function httpBuildQuery(obj)
{
var str = URI.buildQuery(obj);
return str;
}
/**
* ブライトコーブのアクセストークンを取得する
* アクセストークンの有効期限は最大で5分
*/
function getBcToken()
{
var url = API_PATH_BASE + "token.php";
var res = getData(url).then(function(data){
//console.log(data);
var accessToken = data.access_token; // e.g. "ANB7xKhi...UZd"
var tokenType = data.token_type; // e.g. "Bearer"
var expired = data.expires_in; // e.g. 300
BC_TOKEN = accessToken;
//EXPIRED_TIME = moment().add(expired, 'seconds').unix();
// アクセストークン取得に約1秒かかる関係で
// 二回目以降の取得時に有効期限切れになるため補正
EXPIRED_TIME = moment().add(expired, 'seconds').subtract(1, 'seconds').unix();
});
return res;
}
/**
* ブライトコーブのアクセストークンが有効期限内かどうかを判定する
* 有効期限内(つまりアクセストークン取得から5分以内)ならtrueを返す
*/
function isValidBcToken()
{
var now = moment().unix();
//var isValid = moment(now).isSameOrBefore(EXPIRED_TIME);
// 5分ジャストは有効期限切れになる模様(1分毎の自動更新で5回目の更新時にデータ取得に失敗する)
var isValid = moment(now).isBefore(EXPIRED_TIME);
if(isValid){
console.log("current_time:", now);
console.log("expired_time:", EXPIRED_TIME);
console.log("Bc token is still available.");
return true;
}
else {
console.log("current_time:", now);
console.log("expired_time:", EXPIRED_TIME);
console.log("Bc token is expired.");
return false;
}
}
function getIngestUrl(videoId)
{
var url = BC_INGEST_API_URL_BASE + "accounts/" + BC_ACCOUNT_ID + "/videos/" + videoId + "/ingest-requests";
return url;
}
async function updateVideoInfo(videoId, inputs)
{
// アクセストークンが失効したときだけAPIリクエストをする
if(isValidBcToken() === false){
await getBcToken();
}
var accessToken = BC_TOKEN;
$.ajaxSetup({
'headers' : {
"Content-Type" : "application/json",
"Authorization" : "Bearer " + accessToken,
}
});
var inputs = {
"name" : "new_video_title_here",
"cue_points" : [
{
"force_stop": true, // スキップ不可を強制する? 未検証
"time": 0, // タイムコード 秒単位
"name": "", // キューポイント名
"metadata": "", // キーと値のペア
"type": "AD" // タイプ コード -> "DATA", 広告 -> "AD"
}
]
"custom_fields" : {
"hogehoge" : "hoge"
},
"reference_id" : "reference_id_here",
"published_at" : "2017-01-25T06:13:13.813Z",
"geo" : {
"countries" : ["jp"],
"exclude_countries" : false,
"restricted" : true
}
};
inputs = JSON.stringify(inputs);
var url = getVideoUrl(videoId);
var res = patchData(url, inputs);
res.then(function(data){
console.log(data);
});
res.fail(function(jqXHR){
if(jqXHR.status === 404){
var arr = $.parseJSON(jqXHR.responseText);
var message = arr ? "\n" + "[" + arr[0].error_code + "]" + " " + arr[0].message : "";
alert("動画情報が見つかりませんでした" + message);
}
else {
alert("情報の取得に失敗しました");
}
});
}
async function updateVideoImages(videoId, inputs)
{
// アクセストークンが失効したときだけAPIリクエストをする
if(isValidBcToken() === false){
await getBcToken();
}
var accessToken = BC_TOKEN;
$.ajaxSetup({
'headers' : {
"Content-Type" : "application/json",
"Authorization" : "Bearer " + accessToken,
}
});
var imgUrl = "https://www.example.com/images/test.jpg";
var inputs = {
"poster" : {
"url" : imgUrl,
"width" : 300,
"height" : 300
},
"thumbnail" : {
"url" : imgUrl,
"width" : 300,
"height" : 300
},
"images" : [
{
"variant" : "poster",
"url" : imgUrl,
"width" : 300,
"height" : 300
},
{
"variant" : "thumbnail",
"url" : imgUrl,
"width" : 300,
"height" : 300
}
]
};
inputs = JSON.stringify(inputs);
var url = getIngestUrl(videoId);
var res = postData(url, inputs);
res.then(function(data){
console.log(data);
});
res.fail(function(jqXHR){
if(jqXHR.status === 404){
var arr = $.parseJSON(jqXHR.responseText);
var message = arr ? "\n" + "[" + arr[0].error_code + "]" + " " + arr[0].message : "";
alert("動画情報が見つかりませんでした" + message);
}
else {
alert("情報の取得に失敗しました");
}
});
}
$(function(){
var videoId = "1234567890123";
updateVideoImages(videoId);
});