Nuxt.js、Node、Expressを使用したビデオストリーミングアプリの構築
公開: 2022-03-10ビデオはストリームで機能します。 これは、ビデオ全体を一度に送信するのではなく、ビデオ全体を構成する小さなチャンクのセットとしてビデオが送信されることを意味します。 これは、低速ブロードバンドでビデオを視聴するときにビデオがバッファリングする理由を説明しています。これは、受信したチャンクのみを再生し、さらにロードしようとするためです。
この記事は、実際のプロジェクトを構築することで新しいテクノロジーを学びたい開発者を対象としています。バックエンドとして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
フォルダーを使用して、すべてのコンポーネントを作成します。 必要なコンポーネントは、 NavBar
とvideo
コンポーネントの2つだけです。 したがって、フロントエンドのルートフォルダでは次のようになります。
mkdir components cd components touch NavBar.vue touch Video.vue
最後に、 home
やabout
などのすべてのページを作成できるページフォルダ。 このアプリに必要な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
タグがあり、少しスタイルが設定されています。
NavBar
をdefault.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>
ビデオのいずれかをクリックすると、次のようになります。
キャプションファイルの追加
トラックファイルを追加するには、 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