Как создать видео загрузчик при помощи Node.js

Если вы когда-либо загружали видеофайл на сайт, то знаете это чувство когда загрузилось 90% и вы случайно обновляете страницу.

В этом учебном руководстве я покажу, как сделать видео загрузчик для сайта, который может возобновить прерванную загрузку, а также сгенерировать обложку.

Сложность

Введение

Для создания такого загрузчика, сервер должен отслеживать процесс загрузки, что бы была возможность восстановить его при обрыве. Для выполнения этой задачи, мы полностью будем контролировать сервер с помощью Node.js. Создадим запрос на определенные блоки данных, затем HTML-форма будет загружать эти запросы и отправлять необходимую информацию на сервер.

Чтобы обработать эту передачу информации, мы будем использовать Socket.io. Если никогда не слышали о Socket.io, это - платформа для связи в режиме реального времени между Node.js и HTML веб-страницей.

Шаг – 1. HTML разметка

Я сделаю HTML код довольно простым; всё что нам нужно, добавить выбор нужных файлов, текстовое поле для имени и кнопку загрузки. Вот необходимый код:

<body>
<div id="UploadBox">
<h2>Video Uploader</h2>
<span id='UploadArea'>
<label for="FileBox">Choose A File: </label><input type="file" id="FileBox"><br>
<label for="NameBox">Name: </label><input type="text" id="NameBox"><br>

<button type='button' id='UploadButton' class='Button'>Upload</button>
</span>
</div>
</body>

Я добавил весь контент в wrapper; мы будем использовать его позже, чтобы обновить страницу в JavaScript. Я не буду показывать CSS код в этом уроке, но вы можете скачать его из исходных файлов.

Шаг – 2. Кроссбраузерность

HTML5 все еще относительно молод и еще не полностью поддерживается во всех браузерах. Первым делом, мы должны убедиться, что браузер пользователя поддерживает API Файлы HTML5 и класс FileReader.

Класс FileReader позволяет нам открывать и читать части файла и передавать данные серверу в двоичной системе.

window.addEventListener("load", Ready); 
 
function Ready(){
   if(window.File && window.FileReader){ //These are the relevant HTML5 objects that we are going to use
      document.getElementById('UploadButton').addEventListener('click', StartUpload);
      document.getElementById('FileBox').addEventListener('change', FileChosen);
   }
   else
   {
      document.getElementById('UploadArea').innerHTML = "Your Browser Doesn't Support The File API Please Update Your Browser";
   }
}

Код выше добавляет обработчики событий к кнопке и выбору файла в форме. Функция FileChosen просто устанавливает глобальную переменную с файлом – так, чтобы мы могли получить доступ к нему – и заполнили поле имени, чтобы у пользователя была контрольная точка при создании имени файла. Вот функция FileChosen:

var SelectedFile;
function FileChosen(evnt) {
     SelectedFile = evnt.target.files[0];
   document.getElementById('NameBox').value = SelectedFile.name;
 }

Прежде, чем мы перейдем к функции StartUpload, мы должны установить на сервер Node.js и socket.io;

Шаг – 3. Socket.io

Как упоминалось ранее, я буду использовать Socket.io для передачи данных между сервером и HTML файлом. Чтобы загрузить Socket.io, начните установку npm socket.io. Работа socket.io: сервер или клиент испускают события, а затем другая сторона будет загружать эти события в форме функции с опцией передачи данных JSON. Для начала создайте пустой файл JavaScript со следующим содержанием:

var app = require('http').createServer(handler)
  , io = require('socket.io').listen(app)
  , fs = require('fs')
  , exec = require('child_process').exec
  , util = require('util')
 
app.listen(8080);
 
function handler (req, res) {
  fs.readFile(__dirname + '/index.html',
  function (err, data) {
    if (err) {
      res.writeHead(500);
      return res.end('Error loading index.html');
    }
    res.writeHead(200);
    res.end(data);
  });
}
 
io.sockets.on('connection', function (socket) {
   //Обработчики сюда
});

Первые пять строк включают дополнительные библиотеки, следующая строка дает серверу команду использовать порт 8080, а функция-обработчик просто передает содержимое HTML файла пользователю, когда он получает доступ к сайту.

Последние две строки - обработчик socket.io, вызываем его когда кто-то соединяется через Socket.io.

Теперь, мы можем вернуться к HTML файлу и определить некоторые события socket.io.

Шаг – 4. События Socket.io

Чтобы начать использовать Socket.io на нашей странице, сначала мы должны соединиться с ее библиотекой JavaScript.

<script src="/socket.io/socket.io.js"></script>

Данный файл сгенерирован во время выполнения Node.js.

Теперь, мы можем записать функцию StartUpload, которую мы привязали к нашей кнопке:

var socket = io.connect('http://localhost:8080');
var FReader;
var Name;
function StartUpload(){
if(document.getElementById('FileBox').value != "")
{
FReader = new FileReader();
Name = document.getElementById('NameBox').value;
var Content = "<span id='NameArea'>Uploading " + SelectedFile.name + " as " + Name + "</span>";
Content += '<div id="ProgressContainer"><div id="ProgressBar"></div></div><span id="percent">0%</span>';
Content += "<span id='Uploaded'> - <span id='MB'>0</span>/" + Math.round(SelectedFile.size / 1048576) + "MB</span>";
document.getElementById('UploadArea').innerHTML = Content;
FReader.onload = function(evnt){
socket.emit('Upload', { 'Name' : Name, Data : evnt.target.result });
}
socket.emit('Start', { 'Name' : Name, 'Size' : SelectedFile.size });
}
else
{
alert("Please Select A File");
}
}

Первая строка - соединение с Socket.io; затем идут две переменные для чтения и имени файла. В функции мы выполняем проверку, что пользователь выбрал файл, затем создаем FileReader и обновляем DOM с индикатором загрузки.

Метод FileReader вызывают каждый раз, когда он считывает некоторые данные; все, что мы должны сделать, запустить событие Upload и отправить данные на сервер. Наконец, мы запускаем событие Start, которое передает имя файла и его размер на сервер.

Теперь, давайте возвратимся к файлу Node.js и реализуем обработчики для этих двух событий.

Шаг – 5. Управление

События socket.io находятся в обработчике, который мы добавили в последней строке файла Node.js. Первое событие, которое мы реализуем, является событием Start, оно запускается когда пользователь нажимает кнопку Upload.

Я упоминал ранее, что сервер должен управлять данными, что бы была возможность восстановить их при обрыве загрузки. Для начала нужно проверить имя файла который не загрузился, если имена совпадают то будет продолжаться загрузка с момента её обрыва; в противном случае загрузку запустим заново.

Для отслеживания нескольких загрузок, происходящих одновременно, мы должны добавить переменную, чтобы сохранить их все.

socket.on('Start', function (data) { 
      var Name = data['Name'];
      Files[Name] = {  
         FileSize : data['Size'],
         Data   : "",
         Downloaded : 0
      }
      var Place = 0;
      try{
         var Stat = fs.statSync('Temp/' +  Name);
         if(Stat.isFile())
         {
            Files[Name]['Downloaded'] = Stat.size;
            Place = Stat.size / 524288;
         }
      }
      catch(er){} //It's a New File
      fs.open("Temp/" + Name, "a", 0755, function(err, fd){
         if(err)
         {
            console.log(err);
         }
         else
         {
            Files[Name]['Handler'] = fd; //We store the file handler so we can write to it later
            socket.emit('MoreData', { 'Place' : Place, Percent : 0 });
         }
      });
});

Во-первых, мы добавляем новый файл к массиву Files, с данными о размере и суммой загружаемых байтов. Переменная Place хранит данные о расположении загруженных файлов – принимает значение по умолчанию 0. Сначала проверяем, является ли это новой закачкой или нет, затем открываем файл для того, чтобы записать его в папку временных файлов, и запускаем событие MoreData, чтобы запросить следующий раздел данных из HTML.

Теперь, мы должны добавить событие Upload, которое, если помните, вызывают каждый раз, когда считывается новый блок данных.

socket.on('Upload', function (data){
      var Name = data['Name'];
      Files[Name]['Downloaded'] += data['Data'].length;
      Files[Name]['Data'] += data['Data'];
      if(Files[Name]['Downloaded'] == Files[Name]['FileSize']) 
      {
         fs.write(Files[Name]['Handler'], Files[Name]['Data'], null, 'Binary', function(err, Writen){
            //Получение миниатюры здесь
         });
      }
      else if(Files[Name]['Data'].length > 10485760){
         fs.write(Files[Name]['Handler'], Files[Name]['Data'], null, 'Binary', function(err, Writen){
            Files[Name]['Data'] = ""; //Сброс буфера
            var Place = Files[Name]['Downloaded'] / 524288;
            var Percent = (Files[Name]['Downloaded'] / Files[Name]['FileSize']) * 100;
            socket.emit('MoreData', { 'Place' : Place, 'Percent' :  Percent});
         });
      }
      else
      {
         var Place = Files[Name]['Downloaded'] / 524288;
         var Percent = (Files[Name]['Downloaded'] / Files[Name]['FileSize']) * 100;
         socket.emit('MoreData', { 'Place' : Place, 'Percent' :  Percent});
      }
   });

Первые две строки этого кода обновляют буфер с новыми данными, а также загруженную переменную. Мы должны хранить данные в буфере, чтобы они не разрушали сервер из-за перегрузки памяти; производим очистку буфера после каждых 10 мегабайтов.

Первое что оператор будет определять, загружен ли файл полностью, вторая проверка это, достиг ли буфер размера 10 Мбайт, и наконец, запрашиваем MoreData.

Шаг – 6. Отслеживание загрузки

Я создал функцию, чтобы обновить индикатор загрузки и размер файла на странице. В дополнение к этому, событие More Data читает блок данных и передает их на сервер.

Чтобы разбить файл на блоки, мы используем команду Slice API. Так как API находится все еще в разработке, мы должны использовать webkitSlice и mozSlice для браузеров Webkit и Mozilla.

socket.on('MoreData', function (data){
   UpdateBar(data['Percent']);
   var Place = data['Place'] * 524288; 
   var NewFile; //The Variable that will hold the new Block of Data
   if(SelectedFile.webkitSlice)
      NewFile = SelectedFile.webkitSlice(Place, Place + Math.min(524288, (SelectedFile.size-Place)));
   else
      NewFile = SelectedFile.mozSlice(Place, Place + Math.min(524288, (SelectedFile.size-Place)));
   FReader.readAsBinaryString(NewFile);
});
 
function UpdateBar(percent){
   document.getElementById('ProgressBar').style.width = percent + '%';
   document.getElementById('percent').innerHTML = (Math.round(percent*100)/100) + '%';
   var MBDone = Math.round(((percent/100.0) * SelectedFile.size) / 1048576);
   document.getElementById('MB').innerHTML = MBDone;
}

Осталось реализовать перемещение загружаемого файла из папки Временных файлов и генерацию обложки.

Шаг – 7. Обложка

Прежде, чем мы сгенерируем обложку, нужно переместить файл из временной папки. Мы можем сделать это при помощи файловых потоков и метода pump.

var inp = fs.createReadStream("Temp/" + Name);
var out = fs.createWriteStream("Video/" + Name);
util.pump(inp, out, function(){
   fs.unlink("Temp/" + Name, function () { 
      //Перемещение файлов завершено
   });
});

Добавили команду unlink; это удалит временный файл, после его полной загрузки. Теперь с обложкой: используем ffmpeg для генерации миниатюры, так как он может обрабатывать различные форматы. На данный момент нет никаких хороших ffmpeg модулей, поэтому мы будем использовать exec команду, которая позволяет нам выполнять команды Terminal из Node.js.

exec("ffmpeg -i Video/" + Name  + " -ss 01:30 -r 1 -an -vframes 1 -f mjpeg Video/" + Name  + ".jpg", function(err){
   socket.emit('Done', {'Image' : 'Video/' + Name + '.jpg'});
});

Эта ffmpeg команда генерирует одну миниатюру в момент времени 1:30 и сохранит её в папке Video с расширением jpg. Можете отредактировать время миниатюры, изменяя параметр -ss. Как только обложка была сгенерирована, мы запускаем событие Done. Теперь, давайте вернемся к странице HTML.

Шаг – 8. Завершение

Событие Done удалит индикатор загрузки и заменит его обложкой. Поскольку Node.js не установлен как веб-сервер, мы должны определить расположение сервера (например, Apache) в переменной Path, чтобы загрузить изображение.

var Path = "http://localhost/";

socket.on('Done', function (data){
var Content = "Video Successfully Uploaded !!"
Content += "<img id='Thumb' src='" + Path + data['Image'] + "' alt='" + Name + "'><br>";
Content += "<button type='button' name='Upload' value='' id='Restart' class='Button'>Upload Another</button>";
document.getElementById('UploadArea').innerHTML = Content;
document.getElementById('Restart').addEventListener('click', Refresh);
});
function Refresh(){
location.reload(true);
}

Также добавили кнопку загрузки следующего файла.

Перевод статьи How to Create a Resumable Video Uploader in Node.js

Тэги: APIformJSONnode.jssocket.io

Вход

Уважаемый пользователь! Мы обнаружили, что вы используете AdBlock и вынуждены скрыть часть материалов на нашем сайте. Siteacademy существует и развивается за счет доходов от рекламы. Просим внести наш сайт в список исключений или отключить Блокировщик рекламы на нашем сайте.