Статьи

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

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

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


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

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

Это основная концепция; Начнем с формы HTML.


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

01
02
03
04
05
06
07
08
09
10
11
<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>
        
    </div>
</body>

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


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

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

01
02
03
04
05
06
07
08
09
10
11
12
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 :

1
2
3
4
5
var SelectedFile;
function FileChosen(evnt) {
    SelectedFile = evnt.target.files[0];
    document.getElementById(‘NameBox’).value = SelectedFile.name;
}

Прежде чем писать функцию StartUpload , нам нужно настроить сервер Node.js с помощью socket.io; давайте позаботимся об этом сейчас.


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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.


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

1
<script src=»/socket.io/socket.io.js»></script>

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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 + «
        Content += ‘<div id=»ProgressContainer»><div id=»ProgressBar»></div></div><span id=»percent»>0%
        Content += «<span id=’Uploaded’> — <span id=’MB’>0
        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 с хорошим индикатором выполнения.

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

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


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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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;
                socket.emit(‘MoreData’, { ‘Place’ : Place, Percent : 0 });
            }
        });
});

Сначала мы добавляем новый файл в массив Files указанием размера, данных и количества загруженных байтов. Переменная Place хранит, где в файле мы до — по умолчанию 0, что является началом. Затем мы проверяем, существует ли файл (то есть был ли он посередине и остановлен), и соответственно обновляем переменные. Будь то новая загрузка или нет, мы теперь открываем файл для записи в папку Temp/ и MoreData событие MoreData для запроса следующего раздела данных из файла HTML.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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’] = «»;
                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 МБ, и, наконец, мы запрашиваем MoreData , передавая процент выполнения и следующий блок данных для извлечения.

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


Я создал функцию для обновления индикатора выполнения и объема загруженного МБ на странице. Кроме того, событие « More Data считывает блок данных, запрошенный сервером, и передает его на сервер.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
socket.on(‘MoreData’, function (data){
    UpdateBar(data[‘Percent’]);
    var Place = data[‘Place’] * 524288;
    var NewFile;
    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/ и создать миниатюру.


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

1
2
3
4
5
6
7
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.

1
2
3
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-странице и реализуем ее.


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

01
02
03
04
05
06
07
08
09
10
11
12
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!

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