LoginSignup
0
0

ブライトコーブ API サムネイル・ポスター画像を差し替える方法

Last updated at Posted at 2023-06-21

事前準備

以下6点の情報をブライトコーブ管理者に相談して、
事前に準備しておいてください

  1. ブライトコーブ アカウントID
  2. ブライトコーブ クライアントID
  3. ブライトコーブ 秘密鍵
  4. ブライトコーブ API 権限設定
  5. ブライトコーブ テスト用の動画を用意する(動画IDを用意する)
  6. 公開サーバー上にテスト用の画像を置く

アカウント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

どこかで拾ったパーミッション設定の画像を載せておきます。

di-permissions.png

ここまで出来れば、以下のソースコードを使うことで
画像の差し替えが可能になります。

実装

以下のソースコードを、PHPが動くサーバー上に「 bc_image/upload.php 」など、
任意の名前をつけて置いてからGETメソッドでアクセスしてください。
定数の値は適宜、変更してください。

bc_image/upload.php
<?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();

動作確認

image.png

image.png

image.png

つまづきポイント

画像の差し替えには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);
});
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0