Создание приложения для потокового видео с помощью 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 для видео, и это приложение будет иметь два представления: список доступных видео и представление проигрывателя для каждого видео.

Предпосылки

  • Понимание HTML, CSS, JavaScript, Node/Express и Vue.
  • Текстовый редактор (например, VS Code).
  • Веб-браузер (например, Chrome, Firefox).
  • FFmpeg установлен на вашей рабочей станции.
  • Node.js. нвм.
  • Вы можете получить исходный код на GitHub.

Настройка нашего приложения

В этом приложении мы построим маршруты для выполнения запросов из внешнего интерфейса:

  • videos маршрут для получения списка видео и их данных.
  • маршрут для получения только одного видео из нашего списка видео.
  • 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 в папку с ресурсами и назовите его video1 . Вы можете использовать короткие примеры видео в .mp4 , которые можно найти в репозитории Github.

Создайте файл 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!') });

В файл package.json добавлен скрипт для запуска нашего сервера с помощью nodemon.

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

Затем на вашем терминале запустите:

 npm run start

Если вы видите сообщение Listening on port 3000! в терминале, значит сервер работает корректно. Перейдите к https://localhost:5000/video в своем браузере, и вы должны увидеть воспроизводимое видео.

Запросы, которые будут обрабатываться внешним интерфейсом

Ниже приведены запросы, которые мы будем отправлять к бэкэнду из нашего внешнего интерфейса, для обработки которых нам нужен сервер.

  • /videos
    Возвращает массив данных макета видео, который будет использоваться для заполнения списка видео на Home странице в нашем интерфейсе.
  • /video/:id/data
    Возвращает метаданные для одного видео. Используется страницей Player в нашем интерфейсе.
  • /video/: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 и добавим файл Video.js для нашего маршрута /videos . В этом файле нам потребуется 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 и передавать видео фрагментами, поэтому:

Удалите маршрут /video из app.js

Нам нужно три видео, поэтому скопируйте примеры видео из исходного кода руководства в каталог assets/ вашего server проекта. Убедитесь, что имена файлов для видео соответствуют id в массиве videos :

Вернувшись в наш файл 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 в нашем браузере, мы увидим потоковое видео.

Как работает маршрут потокового видео

В маршруте нашего потокового видео написано довольно много кода, поэтому давайте рассмотрим его построчно.

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

Во-первых, из нашего запроса мы получаем id маршрута с помощью req.params.id и используем его для генерации videoPath к видео. Затем мы читаем fileSize используя импортированную файловую систему fs . Для видео браузер пользователя отправит параметр 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, означающий, что ответ содержит частичное содержимое. Это означает, что браузер будет продолжать делать запросы, пока не получит все фрагменты видео.

Что происходит при нестабильных соединениях

Если у пользователя медленное соединение, сетевой поток сигнализирует об этом, запрашивая приостановку источника ввода-вывода до тех пор, пока клиент не будет готов для получения дополнительных данных. Это известно как противодавление . Мы можем сделать еще один шаг в этом примере и посмотреть, как легко расширить поток. Мы также можем легко добавить сжатие!

 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-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 . Итак, в нашей корневой папке интерфейса мы:

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

Наконец, папка страниц, в которой можно создать все наши страницы, такие как home и about нас. Две страницы, которые нам нужны в этом приложении, — это 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 , который отображает Streaming App с небольшим стилем.

Давайте импортируем 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>

Видео компонент

Ниже мы сначала объявляем наш реквизит. Поскольку видеоданные теперь доступны в компоненте, с помощью v-for Vue мы перебираем все полученные данные и для каждого из них отображаем информацию. Мы можем использовать директиву 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/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
Потоковое видео запускается, когда пользователь щелкает миниатюру. (Большой превью)

Добавляем наш файл подписи

Чтобы добавить наш файл трека, мы должны убедиться, что все файлы .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
  • «Понимание титров и субтитров», Screenfont.ca