Nuxt.js、Node、Expressを使用したビデオストリーミングアプリの構築

公開: 2022-03-10
簡単なまとめ↬この記事では、Nuxt.jsとNode.jsを使用してビデオストリーミングアプリを構築します。 具体的には、動画の取得とストリーミング、動画のサムネイルの生成、キャプションと字幕の提供を処理するサーバー側のNode.jsアプリを構築します。

ビデオはストリームで機能します。 これは、ビデオ全体を一度に送信するのではなく、ビデオ全体を構成する小さなチャンクのセットとしてビデオが送信されることを意味します。 これは、低速ブロードバンドでビデオを視聴するときにビデオがバッファリングする理由を説明しています。これは、受信したチャンクのみを再生し、さらにロードしようとするためです。

この記事は、実際のプロジェクトを構築することで新しいテクノロジーを学びたい開発者を対象としています。バックエンドとしてNode.jsを使用し、クライアントとしてNuxt.jsを使用するビデオストリーミングアプリです。

  • Node.jsは、高速でスケーラブルなアプリケーションを構築するために使用されるランタイムです。 これを使用して、ビデオのフェッチとストリーミング、ビデオのサムネイルの生成、およびビデオのキャプションと字幕の提供を行います。
  • Nuxt.jsは、サーバーでレンダリングされたVue.jsアプリケーションを簡単に構築するのに役立つVue.jsフレームワークです。 動画のAPIを使用します。このアプリケーションには、利用可能な動画のリストと各動画のプレーヤービューの2つのビューがあります。

前提条件

  • HTML、CSS、JavaScript、Node / Express、およびVueの理解。
  • テキストエディタ(VS Codeなど)。
  • Webブラウザ(Chrome、Firefoxなど)。
  • ワークステーションにFFmpegがインストールされています。
  • Node.js。 nvm。
  • ソースコードはGitHubで入手できます。

アプリケーションの設定

このアプリケーションでは、フロントエンドからリクエストを行うためのルートを作成します。

  • videosは、ビデオとそのデータのリストを取得するためにルーティングされます。
  • 動画リストから1本の動画のみを取得するルート。
  • ビデオをストリーミングするためのstreamingルート。
  • captionsは、ストリーミングしているビデオにキャプションを追加するためにルーティングされます。

ルートが作成されたら、 Nuxtフロントエンドの足場を作成します。ここで、 Homeとダイナミックplayerページを作成します。 次に、ホームページにビデオデータを入力するためのvideosルートを要求し、 playerページでビデオをストリーミングするための別の要求を要求し、最後にビデオで使用されるキャプションファイルを提供するように要求します。

アプリケーションを設定するために、プロジェクトディレクトリを作成します。

 mkdir streaming-app
ジャンプした後もっと! 以下を読み続けてください↓

サーバーのセットアップ

streaming-appディレクトリに、 backendという名前のフォルダを作成します。

 cd streaming-app mkdir backend

バックエンドフォルダーに、サーバープロジェクトに関する情報を格納するためにpackage.jsonファイルを初期化します。

 cd backend npm init -y

アプリをビルドするには、次のパッケージをインストールする必要があります。

  • nodemonは、変更を加えるとサーバーを自動的に再起動します。
  • expressは、ルートを処理するための優れたインターフェイスを提供します。
  • クライアントとサーバーは異なるポートで実行されるため、 corsを使用するとクロスオリジンリクエストを作成できます。

バックエンドディレクトリに、ストリーミング用のビデオを保持するためのフォルダassetsを作成します。

 mkdir assets

.mp4ファイルをassetsフォルダーにコピーし、 video1という名前を付けます。 Githubレポジトリにある.mp4の短いサンプルビデオを使用できます。

app.jsファイルを作成し、アプリに必要なパッケージを追加します。

 const express = require('express'); const fs = require('fs'); const cors = require('cors'); const path = require('path'); const app = express(); app.use(cors())

fsモジュールは、サーバー上でファイルの読み取りと書き込みを簡単に行うために使用されますが、 pathモ​​ジュールは、ディレクトリとファイルパスを操作する方法を提供します。

次に、。 ./videoルートを作成します。 要求されると、ビデオファイルがクライアントに返送されます。

 // add after 'const app = express();' app.get('/video', (req, res) => { res.sendFile('assets/video1.mp4', { root: __dirname }); });

このルートは、要求されたときにvideo1.mp4ビデオファイルを提供します。 次に、ポート3000でサーバーをリッスンします。

 // add to end of app.js file app.listen(5000, () => { console.log('Listening on port 5000!') });

nodemonを使用してサーバーを起動するためのスクリプトがpackage.jsonファイルに追加されます。

 "scripts": { "start": "nodemon app.js" },

次に、ターミナルで次のコマンドを実行します。

 npm run start

Listening on port 3000! 」というメッセージが表示された場合ターミナルでは、サーバーは正しく機能しています。 ブラウザでhttps:// localhost:5000 / videoに移動すると、ビデオが再生されていることがわかります。

フロントエンドで処理されるリクエスト

以下は、サーバーが処理する必要があるフロントエンドからバックエンドに対して行うリクエストです。

  • /videos
    フロントエンドのHomeにあるビデオのリストを作成するために使用されるビデオモックアップデータの配列を返します。
  • /video/:id/data
    単一のビデオのメタデータを返します。 フロントエンドのPlayerページで使用されます。
  • /video/:id
    指定されたIDでビデオをストリーミングします。 Playerページで使用されます。

ルートを作成しましょう。

ビデオのリストのモックアップデータを返す

このデモアプリケーションでは、メタデータを保持するオブジェクトの配列を作成し、要求されたときにそれをフロントエンドに送信します。 実際のアプリケーションでは、おそらくデータベースからデータを読み取り、このような配列を生成するために使用されます。 簡単にするために、このチュートリアルではそれを行いません。

バックエンドフォルダーにファイルmockdata.jsを作成し、ビデオのリストのメタデータを入力します。

 const allVideos = [ { id: "tom and jerry", poster: 'https://image.tmdb.org/t/p/w500/fev8UFNFFYsD5q7AcYS8LyTzqwl.jpg', duration: '3 mins', name: 'Tom & Jerry' }, { id: "soul", poster: 'https://image.tmdb.org/t/p/w500/kf456ZqeC45XTvo6W9pW5clYKfQ.jpg', duration: '4 mins', name: 'Soul' }, { id: "outside the wire", poster: 'https://image.tmdb.org/t/p/w500/lOSdUkGQmbAl5JQ3QoHqBZUbZhC.jpg', duration: '2 mins', name: 'Outside the wire' }, ]; module.exports = allVideos

上から見ると、各オブジェクトにはビデオに関する情報が含まれています。 ビデオのポスター画像へのリンクを含むposter属性に注意してください。

フロントエンドによって行われるすべてのリクエストの前に/videosが付いているので、 videosルートを作成しましょう。

これを行うには、 routesフォルダーを作成し、 /videosルート用のVideo.jsファイルを追加しましょう。 このファイルでは、 expressが必要であり、エクスプレスルーターを使用してルートを作成します。

 const express = require('express') const router = express.Router()

/videosルートに移動するときに、ビデオのリストを取得したいので、 mockData.jsファイルをVideo.jsファイルに要求して、リクエストを行います。

 const express = require('express') const router = express.Router() const videos = require('../mockData') // get list of videos router.get('/', (req,res)=>{ res.json(videos) }) module.exports = router;

/videosルートが宣言され、ファイルを保存すると、サーバーが自動的に再起動します。 開始したら、https:// localhost:3000 / videosに移動すると、配列がJSON形式で返されます。

単一のビデオのデータを返す

動画リスト内の特定の動画をリクエストできるようにしたいと考えています。 与えたidを使用して、配列内の特定のビデオデータをフェッチできます。 まだVideo.jsファイルにあるリクエストを作成しましょう。

 // make request for a particular video router.get('/:id/data', (req,res)=> { const id = parseInt(req.params.id, 10) res.json(videos[id]) })

上記のコードは、ルートパラメータからidを取得し、それを整数に変換します。 次に、 idに一致するオブジェクトをvideos配列からクライアントに送り返します。

ビデオのストリーミング

app.jsファイルで、クライアントにビデオを提供する/videoルートを作成しました。 このエンドポイントは、リクエストに応じてビデオファイル全体を提供するのではなく、ビデオの小さなチャンクを送信する必要があります。

allVideos配列にある3つのビデオの1つを動的に提供し、ビデオをチャンクでストリーミングできるようにしたいので、次のようにします。

app.jsから/videoルートを削除します。

3つのビデオが必要なので、チュートリアルのソースコードからserverプロジェクトのassets/ディレクトリにサンプルビデオをコピーします。 動画のファイル名がvideos配列のidに対応していることを確認してください。

Video.jsファイルに戻り、ストリーミングビデオのルートを作成します。

 router.get('/video/:id', (req, res) => { const videoPath = `assets/${req.params.id}.mp4`; const videoStat = fs.statSync(videoPath); const fileSize = videoStat.size; const videoRange = req.headers.range; if (videoRange) { const parts = videoRange.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1; const chunksize = (end-start) + 1; const file = fs.createReadStream(videoPath, {start, end}); const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); file.pipe(res); } else { const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(videoPath).pipe(res); } });

ブラウザでhttps:// localhost:5000 / videos / video / outside-the-wireに移動すると、ビデオストリーミングを確認できます。

ストリーミングビデオルートの仕組み

ストリームビデオルートにはかなりのコードが記述されているので、1行ずつ見ていきましょう。

 const videoPath = `assets/${req.params.id}.mp4`; const videoStat = fs.statSync(videoPath); const fileSize = videoStat.size; const videoRange = req.headers.range;

まず、リクエストから、 req.params.idを使用してルートからidを取得し、それを使用してビデオへのvideoPathを生成します。 次に、インポートしたファイルシステムfsを使用してfileSizeを読み取ります。 動画の場合、ユーザーのブラウザはリクエストでrangeパラメータを送信します。 これにより、サーバーはビデオのどのチャンクをクライアントに送り返すかを知ることができます。

一部のブラウザは最初のリクエストで範囲を送信しますが、他のブラウザは送信しません。 送信しない場合、またはその他の理由でブラウザが範囲を送信しない場合は、 elseブロックで処理します。 このコードはファイルサイズを取得し、ビデオの最初のいくつかのチャンクを送信します。

 else { const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(path).pipe(res); }

ifブロックの範囲を含む後続のリクエストを処理します。

 if (videoRange) { const parts = videoRange.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1; const chunksize = (end-start) + 1; const file = fs.createReadStream(videoPath, {start, end}); const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); file.pipe(res); }

上記のコードは、範囲のstart値とend値を使用して読み取りストリームを作成します。 応答ヘッダーのContent-Lengthを、 start値とend値から計算されるチャンクサイズに設定します。 また、HTTPコード206を使用して、応答に部分的なコンテンツが含まれていることを示します。 これは、ブラウザがビデオのすべてのチャンクをフェッチするまでリクエストを出し続けることを意味します。

不安定な接続で何が起こるか

ユーザーが低速接続を使用している場合、ネットワークストリームは、クライアントが追加のデータの準備ができるまでI/Oソースを一時停止するように要求することで信号を送ります。 これは背圧として知られています。 この例をさらに一歩進めて、ストリームを拡張するのがいかに簡単かを確認できます。 圧縮も簡単に追加できます!

 const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize-1; const chunksize = (end-start) + 1; const file = fs.createReadStream(videoPath, {start, end});

上記のように、 ReadStreamが作成され、チャンクごとにビデオを提供していることがわかります。

 const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); file.pipe(res);

リクエストヘッダーにはContent-Rangeが含まれています。これは、フロントエンドにストリーミングするビデオの次のチャンクを取得するために変更される開始と終了です。content content-lengthは送信されるビデオのチャンクです。 また、ストリーミングするコンテンツのタイプであるmp4も指定します。 206の書き込みヘッドは、新しく作成されたストリームのみで応答するように設定されています。

ビデオのキャプションファイルを作成する

これは、 .vttキャプションファイルがどのように見えるかです。

 WEBVTT 00:00:00.200 --> 00:00:01.000 Creating a tutorial can be very 00:00:01.500 --> 00:00:04.300 fun to do.

キャプションファイルには、ビデオでの発言のテキストが含まれています。 また、テキストの各行をいつ表示するかについてのタイムコードも含まれています。 ビデオにキャプションを付けたいので、このチュートリアル用に独自のキャプションファイルを作成しないので、リポジトリのassetsディレクトリにあるキャプションフォルダに移動して、キャプションをダウンロードできます。

キャプションリクエストを処理する新しいルートを作成しましょう。

 router.get('/video/:id/caption', (req, res) => res.sendFile(`assets/captions/${req.params.id}.vtt`, { root: __dirname }));

フロントエンドの構築

システムの視覚的な部分を開始するには、フロントエンドのスキャフォールドを構築する必要があります。

アプリを作成するには、vue-cliが必要です。 コンピューターにインストールしていない場合は、 npm install -g @vue/cliを実行してインストールできます。

インストール

プロジェクトのルートで、フロントエンドフォルダーを作成しましょう。

 mkdir frontend cd frontend

その中で、 package.jsonファイルを初期化し、以下をコピーして貼り付けます。

 { "name": "my-app", "scripts": { "dev": "nuxt", "build": "nuxt build", "generate": "nuxt generate", "start": "nuxt start" } }

次にnuxtをインストールします:

 npm add nuxt

次のコマンドを実行して、Nuxt.jsアプリを実行します。

 npm run dev

Nuxtファイルの構造

Nuxtがインストールされたので、フロントエンドのレイアウトを開始できます。

まず、アプリのルートにlayoutsフォルダーを作成する必要があります。 このフォルダーは、移動先のページに関係なく、アプリのレイアウトを定義します。 ナビゲーションバーやフッターなどはここにあります。 フロントエンドフォルダーに、フロントエンドアプリを起動するときに、デフォルトのレイアウト用にdefault.vueを作成します。

 mkdir layouts cd layouts touch default.vue

次に、 componentsフォルダーを使用して、すべてのコンポーネントを作成します。 必要なコンポーネントは、 NavBarvideoコンポーネントの2つだけです。 したがって、フロントエンドのルートフォルダでは次のようになります。

 mkdir components cd components touch NavBar.vue touch Video.vue

最後に、 homeaboutなどのすべてのページを作成できるページフォルダ。 このアプリに必要な2つのページは、すべての動画と動画情報を表示するhomeと、クリックした動画にルーティングする動的プレーヤーページです。

 mkdir pages cd pages touch index.vue mkdir player cd player touch _name.vue

フロントエンドディレクトリは次のようになります。

 |-frontend |-components |-NavBar.vue |-Video.vue |-layouts |-default.vue |-pages |-index.vue |-player |-_name.vue |-package.json |-yarn.lock

ナビゲーションバーコンポーネント

NavBar.vueは次のようになります。

 <template> <div class="navbar"> <h1>Streaming App</h1> </div> </template> <style scoped> .navbar { display: flex; background-color: #161616; justify-content: center; align-items: center; } h1{ color:#a33327; } </style>

NavBarには、ストリーミングアプリを表示するh1タグがあり、少しスタイルが設定されています。

NavBardefault.vueレイアウトにインポートしてみましょう。

 // default.vue <template> <div> <NavBar /> <nuxt /> </div> </template> <script> import NavBar from "@/components/NavBar.vue" export default { components: { NavBar, } } </script>

default.vueレイアウトには、 NavBarコンポーネントと、作成したページが表示される場所を示す<nuxt />タグが含まれるようになりました。

index.vue (ホームページ)で、 https://localhost:5000/videosにリクエストを送信して、サーバーからすべての動画を取得しましょう。 データを小道具としてvideo.vueコンポーネントに渡し、後で作成します。 しかし今のところ、私たちはすでにそれをインポートしています。

 <template> <div> <Video :videoList="videos"/> </div> </template> <script> import Video from "@/components/Video.vue" export default { components: { Video }, head: { title: "Home" }, data() { return { videos: [] } }, async fetch() { this.videos = await fetch( 'https://localhost:5000/videos' ).then(res => res.json()) } } </script>

ビデオコンポーネント

以下では、最初に小道具を宣言します。 ビデオデータがコンポーネントで利用できるようになったため、Vueのv-forを使用して、受信したすべてのデータを反復処理し、各データについて情報を表示します。 v-forディレクティブを使用して、データをループし、リストとして表示できます。 いくつかの基本的なスタイリングも追加されました。

 <template> <div> <div class="container"> <div v-for="(video, id) in videoList" :key="id" class="vid-con" > <NuxtLink :to="`/player/${video.id}`"> <div : class="vid" ></div> <div class="movie-info"> <div class="details"> <h2>{{video.name}}</h2> <p>{{video.duration}}</p> </div> </div> </NuxtLink> </div> </div> </div> </template> <script> export default { props:['videoList'], } </script> <style scoped> .container { display: flex; justify-content: center; align-items: center; margin-top: 2rem; } .vid-con { display: flex; flex-direction: column; flex-shrink: 0; justify-content: center; width: 50%; max-width: 16rem; margin: auto 2em; } .vid { height: 15rem; width: 100%; background-position: center; background-size: cover; } .movie-info { background: black; color: white; width: 100%; } .details { padding: 16px 20px; } </style>

NuxtLinkには、/ player/ NuxtLinkにルーティングする動的ルートがあることにも注意して/player/video.id

必要な機能は、ユーザーがいずれかのビデオをクリックすると、ストリーミングを開始することです。 これを実現するために、 _name.vueルートの動的な性質を利用します。

その中で、ビデオプレーヤーを作成し、ビデオをストリーミングするためのエンドポイントにソースを設定しますが、リンクが受信したパラメーターをキャプチャするthis.$route.params.nameを使用して、再生するビデオをエンドポイントに動的に追加します。 。

 <template> <div class="player"> <video controls muted autoPlay> <source :src="`https://localhost:5000/videos/video/${vidName}`" type="video/mp4"> </video> </div> </template> <script> export default { data() { return { vidName: '' } }, mounted(){ this.vidName = this.$route.params.name } } </script> <style scoped> .player { display: flex; justify-content: center; align-items: center; margin-top: 2em; } </style>

ビデオのいずれかをクリックすると、次のようになります。

Nuxtビデオストリーミングアプリの最終結果
ユーザーがサムネイルをクリックすると、ビデオストリーミングが開始されます。 (大プレビュー)

キャプションファイルの追加

トラックファイルを追加するには、 captionsフォルダー内のすべての.vttファイルがidと同じ名前であることを確認します。 ビデオ要素をトラックで更新し、キャプションをリクエストします。

 <template> <div class="player"> <video controls muted autoPlay crossOrigin="anonymous"> <source :src="`https://localhost:5000/videos/video/${vidName}`" type="video/mp4"> <track label="English" kind="captions" srcLang="en" :src="`https://localhost:5000/videos/video/${vidName}/caption`" default> </video> </div> </template>

ビデオ要素にcrossOrigin="anonymous"を追加しました。 そうしないと、キャプションのリクエストは失敗します。 ここで更新すると、キャプションが正常に追加されたことがわかります。

復元力のあるビデオストリーミングを構築する際に留意すべき点。

Twitch、Hulu、Netflixなどのストリーミングアプリケーションを構築する場合、考慮すべき点がいくつかあります。

  • ビデオデータ処理パイプライン
    何百万ものビデオをユーザーに提供するには高性能サーバーが必要であるため、これは技術的な課題になる可能性があります。 高い遅延やダウンタイムは、絶対に避けてください。
  • キャッシング
    このタイプのアプリケーション例Cassandra、Amazon S3、AWS SimpleDBを構築するときは、キャッシュメカニズムを使用する必要があります。
  • ユーザーの地理
    ユーザーの地理を考慮して、配布を検討する必要があります。

結論

このチュートリアルでは、Node.jsで、ビデオをストリーミングし、それらのビデオのキャプションを生成し、ビデオのメタデータを提供するサーバーを作成する方法を見てきました。 また、フロントエンドでNuxt.jsを使用して、サーバーによって生成されたエンドポイントとデータを使用する方法も確認しました。

他のフレームワークとは異なり、Nuxt.jsとExpress.jsを使用してアプリケーションを構築するのは非常に簡単で高速です。 Nuxt.jsの優れた点は、ルートを管理し、アプリの構造を改善する方法です。

  • Nuxt.jsの詳細については、こちらをご覧ください。
  • ソースコードはGithubで入手できます。

資力

  • 「HTML5ビデオへのキャプションと字幕の追加」、MDN Web Docs
  • 「キャプションと字幕を理解する」Screenfont.ca