Как создать возобновляющийся видео-загрузчик в Node.js

Как создать возобновляющийся видео-загрузчик в Node.js

От автора: если вы когда-либо выкладывали сравнительно большой видео-файл, то испытывали это ощущение: закончили уже на 90% и нечаянно обновили страницу – и приходится начинать все снова.

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

Детали учебника

Раздел: HTML5 и Node.js

Сложность: Средняя

скачать исходники

Вступление

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

Для создания такого обмена применим Socket.io. Если вы никогда не слышали о Socket.io, то это инфраструктура для коммуникации в режиме реального времени между Node.js и веб-страницей HTML – более полно об этом мы вскоре поговорим.
Таков общий замысел; начнем с формы 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>

Обратите внимание, что я обернул содержимое в span; этим мы воспользуемся позже для обновления разметки страницы с помощью JavaScript. В этом учебнике я не собираюсь разжевывать весь CSS, а вы, если захотите воспользоваться моим исходным кодом, можете его скачать.

Шаг 2: Заставим его работать

HTML5 еще сравнительно нов, и не поддерживается всеми браузерами. Первое, что нужно сделать – это убедиться, что браузер пользователя поддерживает HTML5 File API и класс FileReader.

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

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 install socket.io в окно терминала (подразумевается, что Node.js вы установили), перейдя в директорию этого проекта. 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) {
   //Events will go here
});

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

Последние две строки – это обработчик socket.io, и вызываются, когда кто-то соединяется через Socket.io.
Теперь можно вернуться к файлу HTML и определить несколько событий socket.io.

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

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

<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; далее мы создали две переменные для File Reader’а и название файла, так как собираемся получить к ним глобальный доступ. Внутри этой функции мы сначала убедились, что пользователь выбрал файл, и, если он это сделал, создаем FileReader и обновляем DOM с красивым индикатором выполнения.

Метод FileReader’а onload вызывается каждый раз при считывании каких-либо данных; все, что нужно сделать – это эмитировать событие Upload и послать данные на сервер. Наконец, эмитируем событие Start, передавая серверу Node.js название и размер файла.

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

Шаг 5: Обработка событий

Вам нужно время от времени очищать буфер, или сервер упадет из-за перегрузки памяти.

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

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

Для отслеживания различных выгрузок, происходящих одновременно, нам нужно добавить переменную для хранения. Вверху своего файла добавьте var Files = {};’ Вот код события Start:

socket.on('Start', function (data) { //data contains the variables that we passed through in the html file
      var Name = data['Name'];
      Files[Name] = {  //Create a new Entry in The Files Variable
         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, что означает начало. Затем проверяем, существует ли уже файл (т.е. он был уже на середине и остановился), и соответственно обновляем переменные. Новая ли это выкачка или нет, сейчас мы открываем файл для записи в папку Temp/ и эмитируем событие 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']) //If File is Fully Uploaded
      {
         fs.write(Files[Name]['Handler'], Files[Name]['Data'], null, 'Binary', function(err, Writen){
            //Get Thumbnail Here
         });
      }
      else if(Files[Name]['Data'].length > 10485760){ //If the Data Buffer reaches 10MB
         fs.write(Files[Name]['Handler'], Files[Name]['Data'], null, 'Binary', function(err, Writen){
            Files[Name]['Data'] = ""; //Reset The Buffer
            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});
      }
   });

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

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

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

Шаг 6: Отслеживание прогресса

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

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

socket.on('MoreData', function (data){
   UpdateBar(data['Percent']);
   var Place = data['Place'] * 524288; //The Next Blocks Starting Position
   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;
}

На этой функции загрузчик наконец заканчивается! Все, что нам остается сделать – переместить законченный файл из папки Temp/ и сгенерировать пиктограмму.

Шаг 7: Пиктограмма

Перед генерированием пиктограммы нужно переместить файл из временной папки. Это можно сделать, применив файловые потоки и метод pump. Метод pump принимает потоки read и write и буферизует данные. Вам нужно добавить этот код туда, где в событии Upload я написал ‘Здесь пиктограмма’:

var inp = fs.createReadStream("Temp/" + Name);
var out = fs.createWriteStream("Video/" + Name);
util.pump(inp, out, function(){
   fs.unlink("Temp/" + Name, function () { //This Deletes The Temporary File
      //Moving File Completed
   });
});

Мы добавили команду разъединиться unlink; так временный файл удалится после окончания его копирования. Теперь перейдем к пиктограмме: для генерации пиктограмм мы применим ffmpeg, потому что он способен обрабатывать множество форматов и его легко установить. На момент написания этого текста хороших модулей ffmpeg нет, поэтому мы применим команду exec, позволяющую нам выполнять терминальные команды изнутри 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 не установлен в качестве веб-сервера, вам нужно для загрузки изображения поместить в переменную Path расположение своего сервера (например, Apache).

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);
}

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

Заключение

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

Автор: Gabriel Manricks

Источник: http://net.tutsplus.com/

Редакция: Команда webformyself.

JavaScript&jQuery с нуля до профи

Пройдите пошаговый видеокурс по JavaScript&jQuery

Научиться

Метки: ,

Комментарии Вконтакте:

Комментарии Facebook:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Я не робот.

Spam Protection by WP-SpamFree