От автора: если вы когда-либо выкладывали сравнительно большой видео-файл, то испытывали это ощущение: закончили уже на 90% и нечаянно обновили страницу – и приходится начинать все снова.
В этом учебнике я продемонстрирую, как сделать видео-загрузчик для своего сайта, умеющий возобновлять прерванную выгрузку, а по ее завершении – генерировать пиктограмму.
Вступление
Чтобы сделать загрузчик возобновляемым, серверу нужно отследить, какой объем файла уже выгружен, и уметь продолжить с того места, где выгрузка остановилась. Для выполнения этой задачи мы дадим серверу Node.js полный контроль над запросами отдельных блоков данных, а форма HTML будет считывать эти запросы и отсылать серверу необходимую информацию.
Для создания такого обмена применим Socket.io. Если вы никогда не слышали о Socket.io, то это инфраструктура для коммуникации в режиме реального времени между Node.js и веб-страницей HTML – более полно об этом мы вскоре поговорим.
Таков общий замысел; начнем с формы HTML.
Шаг 1: HTML
Я собираюсь сделать HTML простым и ясным; все, что нам нужно – ввод для выбора файла, текстовое поле для названия и кнопка начала выгрузки. Вот необходимый код:
1 2 3 4 5 6 7 8 9 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> </span> </div> </body> |
Обратите внимание, что я обернул содержимое в span; этим мы воспользуемся позже для обновления разметки страницы с помощью JavaScript. В этом учебнике я не собираюсь разжевывать весь CSS, а вы, если захотите воспользоваться моим исходным кодом, можете его скачать.
Шаг 2: Заставим его работать
HTML5 еще сравнительно нов, и не поддерживается всеми браузерами. Первое, что нужно сделать – это убедиться, что браузер пользователя поддерживает HTML5 File API и класс FileReader.
Класс FileReader дает возможность открывать и читать части файла и передавать данные на сервер как двоичную строку. Вот JavaScript для детекции контуров:
1 2 3 4 5 6 7 8 9 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; давайте сейчас этим и займемся.
Шаг 3: Сервер Socket.io
Как упоминалось ранее, я применю Socket.io для коммуникации между сервером и файлом HTML. Для закачки Socket.io напишите npm install socket.io в окно терминала (подразумевается, что Node.js вы установили), перейдя в директорию этого проекта. Socket.io работает так: как только сервер или клиент «эмитирует» событие, другая сторона улавливает это событие в виде функции с опцией передачи данных JSON туда и обратно. Для начала создайте пустой файл JavaScript и поместите в него следующий код.
1 2 3 4 5 6 7 8 9 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.
Шаг 4: События Socket.io
Чтобы начать использовать на своей странице Socket.io, сначала нужно соединиться с его библиотекой JavaScript. Делаем это тем же образом, которым мы обратились бы к любой библиотеке: сделайте на нее ссылку в области head. Добавьте на страницу следующее, естественно, перед своими скриптами.
1 |
<script src="/socket.io/socket.io.js"></script> |
Не беспокойтесь по поводу получения этого файла, так как он генерируется сервером Node.js во время исполнения. Теперь можно написать функцию StartUpload, которую мы присоединили к своей кнопке:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var socket = io.connect('//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:
1 2 3 4 5 6 7 8 9 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; //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, которое, если вы помните, вызывается каждый раз, когда прочитывается новый блок данных. Вот функция:
1 2 3 4 5 6 7 8 9 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'] = ""; //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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 я написал ‘Здесь пиктограмма’:
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-странице.
Шаг 8: Окончание
Событие Done удалит индикатор выполнения и заменит его на изображение-пиктограмму. Так как Node.js не установлен в качестве веб-сервера, вам нужно для загрузки изображения поместить в переменную Path расположение своего сервера (например, Apache).
1 2 3 4 5 6 7 8 9 10 11 12 |
var Path = "//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
Источник: //net.tutsplus.com/
Редакция: Команда webformyself.