Nuxt3 と tus を使って Vimeo に動画をアップロードする


はじめに

最近は業務で、Vimeo を使った動画配信サービスの開発をしています。Vimeo とは、動画の作成、販売、アップロード、配信、ライブストリーミングなど動画に関わるありとあらゆるサービスを提供する SaaS の 1 つです。今回、Nuxt.js を使ったフロントエンドから Vimeo に対して動画をアップロードする機能を実装したので、知見を残しておこうと思います。

Vimeo への動画アップロード方法を確認する

Vimeo に動画をアップロードするには、以下のような方法があります。

今回は、この中でも tus プロトコルと Vimeo API を使う実装をしてみます。公式の SDK を使うことでフロントエンドから直接アップロードが可能で、内部的にもデフォルトでは tus を使ったアップロードになります。ですが、今回は一度 API サーバーを経由してから Vimeo にアップロードする方法を取るので、SDK は使用しない方向で行きたいと思います。

tus とは

tus とは HTTP 上に構築されたファイルアップロードのためのプロトコルです。各プロジェクトで独自に実装されていたファイルアップロードの機能を共通化してオープンソースとして提供しています。tus プロトコルを使うことで、独自に実装すると大変なアップロードの進捗状況の確認や、ネットワーク瞬断などで途中でアップロードが失敗したときの再開処理などが容易に実装できます。プロトコルなので、クライアントとサーバーの両方が tus プロトコルに沿った実装をする必要があります。アップロードする側のクライアントとして、JavaScirpt クライアントPython クライアント があります。また、サーバー実装用の tus-node-server や Go 製の tusd も公式の GitHub に OSS として公開されています。プロトコルの仕様についても GitHub に公開されているので、興味がある方は見てみると良いかもしれません。

Vimeo では tus がすでに組み込まれている

Vimeo のアップロード用の API には、tus がすでに実装されているので、フロントエンド側が tus のクライアントを実装することで tus を使った動画アップロード処理を容易に実現できます。以下のような tus を使わない動画アップロードの方法もありますが、公式のドキュメントでは tus を使ったアップロードを推奨しています。

Vimeo の tus 以外のアップロード方法

  • 動画の公開 URL を用意して、Vimeo 側から取得してもらう Pull 方式のアップロード
    • たとえば、S3 や Google Drive などに動画データが存在し、URL として公開されている場合に、Vimeo 側から動画データを取得するようなアプローチです。すでにどこかのストレージに動画がある場合は、この方法が一番手っ取り早いです。フロントエンドも SDK を使用して、URL を指定するだけでアップロードが可能です。
  • HTTP の Form ベースの POST アップロード
    • 動画データを Form に乗っけてアップロードする方式です。Form で POST するだけなので tus を使うよりは簡単に実装が可能です。動画データをまるごと POST するので、tus のように chunk で区切ってアップロードする方式と違い、途中でアップロードに失敗したあとの再開や進捗状況を取得などの実装が難しくなります。

Nuxt3 と tus-js-client 使った Vimeo 動画アップロードを実装してみた

業務では Nuxt2を使っているんですが、せっかくなので勉強も兼ねて最近パブリックベータになった、Nuxt3を使って動画アップロードを実装してみます。

Vimeoのアカウント登録

Vimeo でアカウント登録をします。Vimeo Basic のプランでは無料で使うことができます。アカウントを作成できたら、ログインして Vimeo の管理画面にアクセスできることを確認します。

Vimeo APIを利用するためにClientId, Client Secret, Access Tokenを取得する

Vimeo Developer App にアクセスして、My Apps を作成します。作成できると以下のような画面に遷移するので、アップロードの権限を付与してアクセストークンを生成して控えておきます。後ほど、Vimeo API を使ってアップロードするのに使用します。

あわせて、ClientId, Client Secret も控えておきます。

これで、Vimeo 側の準備は OK です。

Nuxt3 のプロジェクトの作成

次に、Nuxt3 のプロジェクトを作成します。Nuxt3 では nuxi という CLI を使うことで、プロジェクトを作成できます。今回はこちらを使います。プロジェクトが作成できたら、VS Code で開いておきます。

npx nuxi init nuxt3-vimeo-upload
cd nuxt3-vimeo-upload
npm install

必要なパッケージをインストール

Vimeo の API にリクエストするための SDK、tus クライアント、型定義をインストールしておきます。

npm install --save vimeo tus-js-client
npm install -D @types/vimeo @types/tus-js-client

Vimeo の ClientId, ClientSecret, AccessToken を環境変数に追加する

Nuxt3 では DotEnv がすでに組み込まれているので、ルートに .env を追加します。さきほど Vimeo で取得した ClientId, ClientSecret, AccessToken を設定しておきます。その後、nuxt.config.ts に追加します。
2つの環境変数の設定オプションがあり privateRuntimeConfig は、後述する API Routes に使用するサーバーサイドの環境変数を定義します。publicRuntimeConfig は、Nuxt のクライアントサイドで使う環境変数を定義します。
今回は、サーバーサイドで Vimeo API にリクエストするので以下のように設定しました。

.env
VIMEO_CLIENT_ID=xxx
VIMEO_CLIENT_SECRET=xxx
VIMEO_ACCESS_TOKEN=xxx
nuxt.config.ts
import { defineNuxtConfig } from 'nuxt3'

// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
    privateRuntimeConfig: {
        VIMEO_CLIENT_ID: process.env.VIMEO_CLIENT_ID!,
        VIMEO_CLIENT_SECRET: process.env.VIMEO_CLIENT_SECRET!,
        VIMEO_ACCESS_TOKEN: process.env.VIMEO_ACCESS_TOKEN!,
    }
})

Nuxt3 の API Routes を使ってサーバーサイドAPIを作成する

Nuxt3 では、API Routes という機能で、API サーバーを作成できます。こちらを使って Vimeo API にアクセスします。フロントエンドからは useFetch() を使うことでこの API にアクセスできます。
まずは、ディレクトリのルートに server フォルダーを作成します。その後、以下のようにファイルを作成して Vimeo API にリクエストするコードを書きます。
直接アップロードするというよりは、まず Vimeo 側に動画の枠を作成します。枠が作成できたらそこにアップロードするための専用のリンクが払い出されるので、それをフロントエンドに渡し、フロントエンドから直接動画をアップロードする流れになります。

server/vimeo/vimeo-client.ts
import config from "#config";
import { Vimeo, RequestOptions } from "vimeo";

/**
 * @see: https://developer.vimeo.com/api/reference/responses/video
 */
export type CreateVideoVimeoResponse = {
  upload: {
    approach: string;
    complete_uri: string;
    form: string;
    link: string;
    redirect_url: string;
    size: string;
    status: "complete" | "error" | "in_progress";
    upload_link: string;
  };
  player_embed_url: string;
};

const vimeo = new Vimeo(
  config.VIMEO_CLIENT_ID,
  config.VIMEO_CLIENT_SECRET,
  config.VIMEO_ACCESS_TOKEN
);

/**
 * Vimeo APIにリクエストする。vimeo クライアントはそのままだとPromiseに対応していないのでラップします。
 */
export function vimeoRequest<T extends any = any>(
  options: RequestOptions
): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    vimeo.request(options, (err, result, statusCode, headers) => {
      if (err) {
        reject(err);
      }
      resolve(result);
    });
  });
}

server/api/createVideoAndGetUploadUrl.ts
import type { IncomingMessage, ServerResponse } from "http";
import { useBody } from "h3";
import {
  vimeoRequest,
  type CreateVideoVimeoResponse,
} from "../vimeo/vimeo-client";

type CreateVideoAndGetUploadUrlBody = {
  size: string;
};

export default async function (req: IncomingMessage, res: ServerResponse) {
  const body = await useBody<CreateVideoAndGetUploadUrlBody>(req);

  console.log("body", body);

  // Vimeoに動画枠を作成する。この動画枠に対してあとからアップロードする。
  const _res = await vimeoRequest<CreateVideoVimeoResponse>({
    path: "/me/videos",
    method: "POST",
    query: {
      upload: {
        approach: "tus",
        size: body.size,
      },
    },
    headers: {
      "Content-Type": "application/json",
      Accept: "application/vnd.vimeo.*+json;version=3.4",
    },
  });

  console.log(_res);

  return {
    uploadLink: _res.upload.upload_link,
    status: _res.upload.status,
    embedUrl: _res.player_embed_url,
  };
}

動画をアップロードするためのフロントエンドを実装する

サーバーサイドの API ができたので、これをフロントエンドからリクエストして動画アップロード処理を実装します。tus-js-client を使って、アップロード処理と動画アップロード中の進捗状況の表示します。また、アップロード後の動画表示までを実装しています。画面は tailwindcss を使いました。
Vue 3.2 から使える script setup 構文で Composition API で実装してみました。React Hooks のような書き味で、状態管理や computed などの処理を記述できるのがいいですね。

app.vue
<script setup lang="ts">
import { Upload } from 'tus-js-client';

const percentageState = useState('percentage', () => '0');
const uploadingState = useState<'none' | 'uploading' | 'transcoding' | 'uploaded'>('uploading', () => 'none');
const embedUrlState = useState('embedUrl', () => '');

const percentage = computed(() => {
  return percentageState.value + '%';
});
const uploading = computed(() => {
  return uploadingState.value;
})
const embedUrl = computed(() => {
  return embedUrlState.value;
})

/**
 * 動画データをVimeoにアップロードする
 */
async function videoUpload(event: Event) {
  if (event.target instanceof HTMLInputElement && event.target.files && event.target.files.length > 0) {
    const file = event.target.files[0];
    
    uploadingState.value = 'uploading';

    // アップロード用のリンクを取得する
    const { data } = await useFetch('/api/createVideoAndGetUploadUrl', {
      method: 'POST',
      body: {
        size: file.size.toString(),
      },
    });

    const { uploadLink } = data.value;

    // tusクライアントでアップロード
    const upload = new Upload(file, {
      uploadUrl: uploadLink,
      onError: (e) => {
        throw e;
      },
      onProgress: (bytesUploaded: number, bytesTotal: number) => {
        // 動画のアップロードの進捗状況を%で取得してStateに反映させます。
        const _percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
        console.log('percentage', _percentage);
        percentageState.value = _percentage;
      },
      onSuccess: async () => {
        uploadingState.value = 'transcoding';
        // Vimeo側でトランスコーディングがあるので、簡易的に40秒ほどまってから動画の埋め込みURLを取得します。
        // 本当はVimeoに定期的にリクエストすることで動画のステータスを取得できますが、今回は割愛しています。
        await new Promise((resolve) => setTimeout(resolve, 40000));
        
        uploadingState.value = 'uploaded';
        // 埋め込みURLを取得
        embedUrlState.value = data.value.embedUrl;
      },
    });

    upload.start();
  }
}
</script>

<template>
  <div class="container mx-auto w-3/5">
    <label v-if="uploading === 'none'"
        class="flex justify-center w-full h-32 px-4 mt-12 transition bg-white border-2 border-gray-300 border-dashed rounded-md appearance-none cursor-pointer hover:border-gray-400 focus:outline-none">
        <span class="flex items-center space-x-2">
            <span class="font-medium text-gray-600">
                <span class="text-blue-600 underline">ファイルを選択</span>
            </span>
        </span>
        <input @change="videoUpload" type="file" name="file_upload" class="hidden">
    </label>
    
    <div v-if="uploading === 'uploading' || uploading === 'transcoding'" class="relative pt-1 mt-12">
      <div class="flex mb-2 items-center justify-between">
        <div>
          <span class="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full text-pink-600 bg-pink-200">
            {{ uploading === 'uploading' ? 'アップロード中' : 'トランスコーディング中' }}
          </span>
        </div>
        <div class="text-right">
          <span class="text-xs font-semibold inline-block text-pink-600">
            {{ percentage }}
          </span>
        </div>
      </div>
      <div class="overflow-hidden h-2 mb-4 text-xs flex rounded bg-pink-200">
      <div :style="{ width: percentage }" class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-pink-500"></div>
      </div>
    </div>

    <div v-if="uploading === 'uploaded'" style="padding:56.25% 0 0 0;position:relative;">
      <iframe :src="embedUrl" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="テスト動画" />
    </div>
  </div>
</template>

npm run dev -- -o で実行してみました。http://localhost:3000 にアクセスして動作を確認しました。動画をアップロードし画面に進捗状況を見せた後、Vimeo からアップロードした動画を取得し表示しています。

このように tus を使うことで、アップロードや進捗状況の表示などを簡単にフロントエンドで実装できます。

まとめ

Nuxt3 と tus-js-client を使った動画アップロードの実装をしました。Vimeo のようなすでに tus プロトコルが実装されている SaaS を使うことで動画アップロード機能を比較的簡単に実現できました。Vimeo を使わなくとも tusdtus-node-serverなどのパッケージを用いることで自前のサーバーに tus を導入も可能なので、こちらも試してみたいと思いました。

この記事の実装は GitHub リポジトリで公開しています。

https://github.com/briete/nuxt3-vimeo-upload-sample

参考