動画の結合処理を実装したくて試行錯誤の末に、MediaConvertで頑張った話。

## ****この記事に期待できること****

複数の動画を結合する処理の流れがわかる
****AWS Elemental MediaConvert****の扱い方がわかる
**AWS Lambda**のメリットと使い所がわかる

##  なぜ、「動画の結合」をしたいと思ったか？

実は私、パントマイムのパフォーマー＆芝居役者として活動してまして、今は映像制作を仲間と一緒にやっています。こんな感じのチャンネル↓
『アニメOPっポイヤツ』
[https://www.youtube.com/c/Poiyatsu](https://www.youtube.com/c/Poiyatsu)
![ポイヤツ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984421/3cfb4c39-b53b-7757-245b-d6855f1857ea.jpeg)




「映像作りって大変だけど、楽しいなー。視聴者さんとも一緒に作れたら、もっと楽しいのになぁ」と考えていた所、
**ユーザーさんの投稿動画と、私「ハナムラ」の動画を合体させると１本のストーリーになるような映像が作れるアプリ**
ができたら楽しいかも！と思いついたのがきっかけです。
→こちらについては、:movie_camera: [MIME MOVIE（マイムムービー）](https://mime-movie.com) :tophat:というアプリをリリースしました！


## ****AWS Elemental MediaConvert**** とは？

「ブロードキャストグレードの変換品質（放送局レベルの品質）、幅広いフォーマット対応などの特徴をもった動画変換サービス」

簡単に言うと、**色んな形式の動画を、あらゆるフォーマットに変換**してくれます！
公式サイトは[こちら](https://aws.amazon.com/jp/mediaconvert/)

**サービス例**
- 大規模で高解像度な動画の配信を可能に（映画やライブ映像）
- 携帯で撮った映像をストリーミング配信できる形に変換

**使っている企業例**

- テレビ局
- 動画配信会社

ただ今回はそのメイン機能ではなく、マイナー機能「結合」に目をつけてみました！（マイナーが故の苦労もありました、、）

**Media Convertの基本的な使い方（GUI上）**
AWSはGUIが見やすいので、GUIで直接動かして動画を結合する作業は意外と簡単です！
基本の流れは、

「入力設定」
処理を加えたい動画を入力
（時間指定して、特定の部分を切り取るといった設定も可能。）

「出力設定」
どんな動画にしたいかを設定
（ファイル形式やフレームレート、アスペクト比など）

この二つを設定して、作成ボタンを押せば、後はMediaConvertが頑張ってくれます！
ただ、****「ユーザーが動画を投稿してくれたら、それに対応する特定の動画と結合するようにMediaConvertに指示を出す仕組みを作る」****
ためには少し工夫が必要だったので、構成図を交えて説明していきます！

※この技術はプログラミング初学者が独自に考えた解決策の一例です。不都合な点やより良い方法があるかもしれません。あくまで参考程度にして頂ければと思います。


## インフラ構成図
フロントエンド　React
バックエンド　Ruby on Rails（APIモード）
インフラ　AWS
![インフラ構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984421/00563a12-ad72-7070-e838-891e08ec60ee.png)
今回のポイントはこちら！
![インフラ構成図-動画加工部分.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984421/08a41cb1-1e0d-02d9-0144-9285cc103eb9.png)

## ユーザーアクションの流れに合わせて説明！
アクションの流れはこちら。
![アセット 1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984421/52695a21-d6a9-ef59-c1f8-35f366331295.jpeg)

では、セクションに分けて説明していきます！
### ①アップロードボタンを押して投稿動画を添付する
![ユーザーフロー~動画添付.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984421/1f5da94c-6ced-fa6b-9b03-f2f9b7d1f2dc.png)
React→動画を添付する処理
Ruby on Rails→アップロードする時のファイル名を作成する処理

フロントエンド(React）
```tsx
export const UploadUserVideo= (id :number) => {
  const {isAuthenticated,getAccessTokenSilently } = useAuth0();
  const { setAccessToken } = useContext(Auth0Context);
  const [createdFileName, setCreatedFileName] = useState<string>("");
  const [selectedFile, setSelectedFile] = useState<File | null>(null)
  const content_video_id = id["id"]
  const navigate = useNavigate()

  /*S3に動画をダイレクトアップロードできるように、アップロード権限が付与されたIAMユーザーを設定、
  S3バケット、リージョンの指定*/
  const AWS_ACCESS_KEY = process.env.REACT_APP_AWS_ACCESS_KEY_ID;
  const AWS_SECRET_KEY = process.env.REACT_APP_AWS_SECRET_ACCESS_KEY;
  const BUCKET = process.env.REACT_APP_USER_BACKET;
  const REGION = process.env.REACT_APP_REGION;
  const s3 = new AWS.S3();

  AWS.config.update({
    accessKeyId: AWS_ACCESS_KEY,
    secretAccessKey: AWS_SECRET_KEY,
    region: REGION,
  });

    /*ファイル添付時に、バックエンド側にも指示出し→
    コンテンツ番号とユーザーIDを組み合わせた一意のファイル名が返ってくる*/
    const uploadFile = async (file: object) => {
      const token = isAuthenticated ? await getAccessTokenSilently() : null;
      setAccessToken(token);
      console.log(token);
      const res = await axios.get(`${REST_API_URL}/user/user_videos/${content_video_id}`,{
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
      });
      /*ファイル名はあとで使うので、useStateにセットする*/
      const upload_file_name = await res.data
      setCreatedFileName(upload_file_name["key"]);

 
```

バックエンド（Ruby on Rails）

```ruby
def show
      #authorize([:user, UserVideo])
      set_content_video
      render json: { key: "#{@content_video.number}_content_user_#{current_user.id}" }
  end

def set_content_video
    @content_video = ContentVideo.find(params[:id])
end

```
### ②結合動画作成ボタンを押す
![ユーザーフロー〜動画作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984421/cd6f7050-e56d-56e2-9e48-cd134dc69a2d.png)
**１、動画をS３にダイレクトアップロード**

```tsx
   /*添付されたファイルを取り出して、useStateにセットする*/
    const onFileInputChange = async (event :React.ChangeEventHandler<HTMLInputElement> | undefined) => {
      const file = event.target.files[0]
      setSelectedFile(file)
      await uploadFile(file)
    }

    /*添付ファイル、ファイル名をセットしてS3にアップロード→完成版視聴画面に遷移*/
    const handleSubmission = () => {
      try {
        const params = {
          Bucket: BUCKET,
          Key: createdFileName,
          ContentType: selectedFile.type,
          Body: selectedFile,
        };
        const res = s3.putObject(params).promise();
        console.log(res)
        navigate('/created_video', { state: createdFileName })
      } catch (error) {
        console.log(error);
        return;
      }
    }
```
**２、完成版動画を視聴する画面に遷移**
**３、完成版動画が保管されるS3へのURLをVideoタグに埋め込む**

完成版動画を視聴する画面

```tsx
export const   SetUserCreatedVideo = memo(() => {
  const location = useLocation()
  const  user_created_file_name = location.state;
    const videoRef = useRef<HTMLVideoElement>(null);
    useEffect(() => {
        videoRef.current?.play();
    }, []);
    return (
      <div className="set-user-created-video-wrapper">
        <LoadingCountButton />
        <h1 className="completed-video-card-text" >Now Showing</h1>
        <div className="completed-video-design-frame">
          <video className="user-created-video"  playsInline controls ref={videoRef} >
	　　　　　　　　　　　　　　　　　{/* user_created_file_name は、rails側で作成したファイル名。 */}
            <source src={`https://${process.env.REACT_APP_COMPLETED_BACKET}.s3.${process.env.REACT_APP_REGION}.amazonaws.com/${user_created_file_name}_completed.mp4`} type="video/mp4" />
          </video>
        </div>
      </div>
    );
})
```

ここまでで、フロントサイドでは**完成版動画を見る準備が整った**状態です。
### ③MediaConvert&Lambdaでの動画結合処理
ではここから、メインの動画結合処理の説明に入っていきます！
![ユーザーフロー〜MediaConvert.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2984421/1e103ccf-ad8a-7651-62df-5742db146b1f.png)

### AWS Lambdaとは？
さて、ここでは**AWS Lambda**というAWSのサービスを使っております。
まずはその説明から！



💡AWS Lambda は、サーバーレスでイベント駆動型のコンピューティングサービスであり、サーバーのプロビジョニングや管理をすることなく、事実上あらゆるタイプのアプリケーションやバックエンドサービスのコードを実行することができます💡


要は、**「実行したい処理を、簡単に、好きな言語を使って実装したい」**
ならピッタリのサービスです！
公式サイトは[こちら](https://aws.amazon.com/jp/lambda/)

今回、バックエンドはRubyを使用していますが、AWS LambdaではPythonを使用しています。この処理に関して、Pythonのメソッドが使いやすかったからですが、このように**手軽に別言語を使用できる**のも、lambdaの魅力です！

また、**プログラム実行時にのみ利用料金が発生する仕組み**となっているため、常駐サーバーと比べて、非常に**コストダウン**できるというのも魅力です！


さてさて、本題に戻ります。
AWS Lambdaは、**なんらかのイベントが起きたら、あらかじめ設定した処理（機能＝関数）が実行される環境**とも言い換えることができますが、

今回はそのイベントを
**ユーザー投稿動画を保管するS3に、動画がアップロードされたら**に設定しました！
※これはAWS LambdaのGUI上で簡単に設定することができます。


AWS Lambdaで行う処理はこちらになります↓
#### ①ユーザーがアップロードした動画のファイル名（先頭の３桁）から、結合するコンテンツ動画を特定

```python
import json
import urllib.parse
import boto3
import os

USER_BACKET = "*******"
CONTENTS_BACKET = "*******"

def lambda_handler(event, context):
    #アップロードされたユーザー動画のファイル名を取り出す。
    s3_source_key = event['Records'][0]['s3']['object']['key']
    #ユーザー動画のファイル名の先頭３文字はコンテンツ番号が振ってあるので、切り取ってcontent_numberとして扱う。
    s3_source_basename = os.path.splitext(os.path.basename(s3_source_key))[0]
    content_number = s3_source_basename.split('_')[0]
    # 結合させたいコンテンツ動画のファイル名を作成
    s3_content_key = content_number + "_content.mp4"
```

#### ②メディアコンバートを動かすための指示書をS3から取ってきて、ユーザー動画の場所とコンテンツ動画の場所を書き込む

```python
  #MediaConvertの指示書に書き込むために、ユーザー投稿動画と結合させたいコンテンツ動画の保管場所URLを作成
    inputFile_user = "s3://" + USER_BACKET + "/" + s3_source_key
    inputFile_content = "s3://" + CONTENTS_BACKET + "/" + s3_content_key

    s3_client = boto3.client('s3')
    # S3に保管しておいた、MediaConvertに送る指示書であるconnect_user_content.jsonというファイルを開く。
    response = s3_client.get_object(Bucket="connect-user-content",Key="connect_user_content.json")
    body = response['Body'].read()
    # 対象となるユーザー投稿動画とコンテンツ動画を指示書に書き込む。
    job_object = json.loads(body.decode('utf-8'))
    job_object["Inputs"][0]["FileInput"] = inputFile_user
    job_object["Inputs"][1]["FileInput"] = inputFile_content

```

#### ③その指示書を添えて、mediaConvertに作成指示を送る。
※作成権限を持ったロールも付与

```python
 #作った指示書を添えて、mediaConvertに作成指示を送る。※作成権限を持ったロールも付与
mediaconvert =  boto3.client('mediaconvert', region_name='*******', endpoint_url='https://*******.mediaconvert.*******.amazonaws.com')

    response = mediaconvert.create_job(
        Queue='arn:aws:mediaconvert:ap-northeast-1:992946812485:queues/Default',
        Role='arn:aws:iam::992946812485:role/video_upload_download_lambda_role',
        Priority = 0,
        StatusUpdateInterval = 'SECONDS_60',
        AccelerationSettings = {"Mode": "DISABLED"},
        Settings=job_object
    )
```

あらかじめ作っておいたMediaConvertの指示書がこちらになります↓
必要な情報は先に全部記載しています。
動画のファイル形式、画面サイズ、コーデック、フレームレート
音声のファイル形式、完成した動画の保管先、完成した動画のファイル名
などなど。

**入力する動画の欄を二つ作って、書き込めるようにしておく所**がポイントになります！
```json
{
  "TimecodeConfig": {
    "Source": "ZEROBASED"
  },
  "OutputGroups": [
    {
      "CustomName": "connect_video",
      "Name": "File Group",
      "Outputs": [
        {
          "ContainerSettings": {
            "Container": "MP4",
            "Mp4Settings": {}
          },
          "VideoDescription": {
            "Width": 1920,
            "Height": 1080,
            "CodecSettings": {
              "Codec": "H_264",
              "H264Settings": {
                "ParNumerator": 16,
                "FramerateDenominator": 1,
                "MaxBitrate": 8000000,
                "ParDenominator": 9,
                "FramerateControl": "SPECIFIED",
                "RateControlMode": "QVBR",
                "FramerateNumerator": 30,
                "SceneChangeDetect": "TRANSITION_DETECTION"
              }
            }
          },
          "AudioDescriptions": [
            {
              "AudioSourceName": "Audio Selector 1",
              "CodecSettings": {
                "Codec": "AAC",
                "AacSettings": {
                  "Bitrate": 96000,
                  "CodingMode": "CODING_MODE_2_0",
                  "SampleRate": 48000
                }
              }
            }
          ],
          "Extension": "mp4",
          "NameModifier": "_completed"
        }
      ],
      "OutputGroupSettings": {
        "Type": "FILE_GROUP_SETTINGS",
        "FileGroupSettings": {
          "Destination": "******"
        }
      }
    }
  ],
  "Inputs": [
    {
      "AudioSelectors": {
        "Audio Selector 1": {
          "DefaultSelection": "DEFAULT"
        }
      },
      "VideoSelector": {},
      "TimecodeSource": "ZEROBASED",
      "FileInput": ""
    },
    {
      "AudioSelectors": {
        "Audio Selector 1": {
          "DefaultSelection": "DEFAULT"
        }
      },
      "VideoSelector": {},
      "TimecodeSource": "ZEROBASED",
      "FileInput": ""
    }
  ]
}
```

## ここがPOINT!

今回、**JSONファイルを使ってMediaConvertに指示出しをした所**が大きなポイントになります。

というのも、Lambdaを使ってMediaConvertを動かそうとした場合、オーソドックスに実装を考えると、MediaConvertの**ジョブテンプレート**という機能を使おうという発想に至ります。
これは、あらかじめ加工処理の内容を、GUIを用いて設定しておくものです。
後は、このテンプレートを動かす処理だけLambdaで指示を送ればOK!という簡単なものなのですが、ここで「結合」処理はマイナーな機能であることの弊害が出てきます、、

というのも、この「ジョブテンプレート」の仕様だと**入力動画欄を一つしか設定できず、事実上、動画の結合処理はできない**という壁にぶち当たりました、、！なんてことだ、、！

ただ、調査を進めていくと、JSONファイルで全て指示出しをする形であればいけるという形に辿り着いたため、今回の仕様になりました！
JSONファイルを一から書き上げるなんて大変すぎる、、と思われた方がいらっしゃるかと思いますが、MediaConvertは、過去に行ったジョブの設定内容をJSONでエクスポートできる機能がありますので、一度GUIで理想の設定をして作成処理をすれば、簡単に取得できます！

## 制作秘話

実は初めは、FFmpegを使おうとしていました。というのもMediaConvertは、動画のファイル変換がメインで、結合処理がうまく実装できるかわからないと思っていたからです。
**FFmpegとは？動画と音声を記録・変換・再生するためのフリーソフトウェア**

**ではどのように、MediaConvertにたどり着いたのか？というとこんな感じです↓**

 
streamio-ffmpegという人気gemを使おうと進めていた所、このgemには動画結合の機能は入っておらず、コマンドラインを叩くしかないことが判明
⇩
S3を利用して、web上でFFmpegのコマンドラインを使えるような環境を作って、lambdaでコマンドラインを叩くコードを書いたものの、返ってくるデータがどう頑張っても破損している。
⇩
壊れてしまう原因がブラックボックス状態であったため、たとえ上手くいったとしても、これで実装すべきではないかもと判断
⇩
**MediaConvertで頑張り抜くと決心！！**


もちろん、FFmpegで上手くいかなったのは私の力不足が原因なので、実装できる形はあると思いますが、ユーザーさんがどんなファイル形式で送ってきても対応できる安心感や、動画の処理時間が安定している所を考えると、結果的に、MediaConvertを使ってよかったなと思います。

## 最後に
私は今まで、人に喜んでもらえるコンテンツを作ろうと思った時、「舞台、ダンス、大道芸」といった、**目の前のお客様を楽しませる手段**に力を注いできました。
ただ、コロナという事態を受けて、映像作品を作ってYoutubeで発信するということを続けた結果、**僕らのパフォーマンスは、こんなに多くの人に楽しんでもらえる可能性を持っていたのか！**　と感動した覚えがあります。
そして今、webアプリ開発の挑戦を通して **お客様と一緒に作れるという可能性** にワクワクしています！
プログラミング歴10ヶ月といった初学者ですが、面白いものが作れるように今後も色々挑戦していきます:v: