HTML5 <audio>/<video> и транскодирование в реальном времени с помощью FFMPEG
Таким образом, с моего веб-сервера, я хотел бы использовать FFMPEG для перекодирования медиа-файла для использования с HTML <audio>
или же <video>
тег. Достаточно легко, верно?
Преобразование должно происходить в режиме реального времени, когда HTTP-клиент запрашивает преобразованный файл. В идеале файл должен передаваться обратно клиенту HTTP во время его транскодирования (а не в конце, так как это может занять некоторое время, прежде чем любые данные начнут отправляться обратно).
Это было бы хорошо, за исключением того, что в современных браузерах тег HTML5 аудио или видео запрашивает мультимедийный файл в нескольких HTTP-запросах с Range
заголовок. Смотрите этот вопрос для деталей.
В этом вопросе, связанном выше, вы можете видеть, что Safari запрашивает странные куски файла, включая последние несколько байтов. Это создает проблему, заключающуюся в том, что веб-сервер ДОЛЖЕН ждать завершения преобразования, чтобы доставить последние байты файла, чтобы они соответствовали Range
запрос.
Итак, мой вопрос, мой ход мысли правильный? Есть ли лучший способ доставки транскодирующего контента на <audio>
или же <video>
тег, который не будет включать ожидание завершения всего преобразования? Заранее спасибо!
4 ответа
Недавно я столкнулся с той же проблемой, так как хочу показать свою библиотеку браузерам. Удивительно, но идея отправить поток через ffmpeg и доставить на лету работает довольно хорошо. Основная проблема заключалась в поддержке поиска...
Ниже вы найдете фрагменты кода в Python, использующие Flask для решения проблемы:
Нам нужна функция для потоковой передачи контента:
@app.route('/media/<path:path>.ogv')
def media_content_ogv(path):
d= os.path.abspath( os.path.join( config.media_folder, path ) )
if not os.path.isfile( d ): abort(404)
start= request.args.get("start") or 0
def generate():
cmdline= list()
cmdline.append( config.ffmpeg )
cmdline.append( "-i" )
cmdline.append( d );
cmdline.append( "-ss" )
cmdline.append( str(start) );
cmdline.extend( config.ffmpeg_args )
print cmdline
FNULL = open(os.devnull, 'w')
proc= subprocess.Popen( cmdline, stdout=subprocess.PIPE, stderr=FNULL )
try:
f= proc.stdout
byte = f.read(512)
while byte:
yield byte
byte = f.read(512)
finally:
proc.kill()
return Response(response=generate(),status=200,mimetype='video/ogg',headers={'Access-Control-Allow-Origin': '*', "Content-Type":"video/ogg","Content-Disposition":"inline","Content-Transfer-Enconding":"binary"})
Тогда нам нужна функция для возврата продолжительности:
@app.route('/media/<path:path>.js')
def media_content_js(path):
d= os.path.abspath( os.path.join( config.media_folder, path ) )
if not os.path.isfile( d ): abort(404)
cmdline= list()
cmdline.append( config.ffmpeg )
cmdline.append( "-i" )
cmdline.append( d );
duration= -1
FNULL = open(os.devnull, 'w')
proc= subprocess.Popen( cmdline, stderr=subprocess.PIPE, stdout=FNULL )
try:
for line in iter(proc.stderr.readline,''):
line= line.rstrip()
#Duration: 00:00:45.13, start: 0.000000, bitrate: 302 kb/s
m = re.search('Duration: (..):(..):(..)\...', line)
if m is not None: duration= int(m.group(1)) * 3600 + int(m.group(2)) * 60 + int(m.group(3)) + 1
finally:
proc.kill()
return jsonify(duration=duration)
И, наконец, мы взломали это в HTML5 с помощью videojs:
<!DOCTYPE html>
<html>
<head>
<link href="//vjs.zencdn.net/4.5/video-js.css" rel="stylesheet">
<script src="//vjs.zencdn.net/4.5/video.js"></script>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
</head>
<body>
<video id="video" class="video-js vjs-default-skin" controls preload="auto" width="640" height="264">
</video>
<script>
var video= videojs('video');
video.src("media/testavi.avi.ogv");
// hack duration
video.duration= function() { return video.theDuration; };
video.start= 0;
video.oldCurrentTime= video.currentTime;
video.currentTime= function(time)
{
if( time == undefined )
{
return video.oldCurrentTime() + video.start;
}
console.log(time)
video.start= time;
video.oldCurrentTime(0);
video.src("media/testavi.avi.ogv?start=" + time);
video.play();
return this;
};
$.getJSON( "media/testavi.avi.js", function( data )
{
video.theDuration= data.duration;
});
</script>
</body>
Рабочий пример можно найти по адресу https://github.com/derolf/transcoder.
Dero
Спасибо за ответ Камило. Я внимательно посмотрел на HTTP-спецификацию, касающуюся запроса Range, и обнаружил:
The header SHOULD indicate the total length of the full entity-body, unless
this length is unknown or difficult to determine. The asterisk "*" character
means that the instance-length is unknown at the time when the response was
generated.
Так что на самом деле это всего лишь вопрос тестирования реакции браузеров на ответы Content-Range: bytes 0-1/*
, например. Я дам вам знать, что происходит.
Я знаю, что это старая ветка, но я все равно отправлю ее, если кто-то найдет ее и понадобится помощь.
Ответ user3612643 правильный, это решает проблему поиска. Однако это создает новую проблему. Текущее время больше не правильное. Чтобы исправить это, нам нужно скопировать оригиналcurrentTime
функция.
Теперь каждый раз при вызове video.js currentTime
(без параметров) он вызовет oldCurrentTime
который является оригиналом currentTime
функция. Остальное совпадает с ответом пользователя3612643 (спасибо!). Это работает с новейшим video.js (7.7.6)
video = videojs("video");
video.src({
src: 'http://localhost:4000/api/video/sdf',
type: 'video/webm'
});
// hack duration
video.duration= function() {return video.theDuration; };
video.start= 0;
// The original code for "currentTime"
video.oldCurrentTime = function currentTime(seconds) {
if (typeof seconds !== 'undefined') {
if (seconds < 0) {
seconds = 0;
}
this.techCall_('setCurrentTime', seconds);
return;
}
this.cache_.currentTime = this.techGet_('currentTime') || 0;
return this.cache_.currentTime;
}
// Our modified currentTime
video.currentTime= function(time)
{
if( time == undefined )
{
return video.oldCurrentTime() + video.start;
}
video.start= time;
video.oldCurrentTime(0);
video.src({
src: "http://localhost:4000/api/video/sdf?start=" + time,
type: 'video/webm'
});
video.play();
return this;
};
// Get the dureation of the movie
$.getJSON( "http://localhost:4000/api/video/sdf/getDuration", function( data )
{
video.theDuration= data.duration;
});
AFAIK вы можете кодировать в стандартный вывод в ffmpeg. Таким образом, вы можете настроить свой HTTP-сервер на:
- начать кодирование в кэш, когда GET получен.
- Поток запрошенный диапазон байтов для клиента.
- заполнение буфера и использование его для последующих диапазонов.
Я не знаю, но я думаю, что вы можете уйти, не зная длины последнего потока.
С другой стороны, я думаю, что это склонно к DoS.
Это должно быть выполнимо через VLC, я смог заставить его работать, настроив VLC для размещения большого файла avi и перекодировать его в OGG, тогда мой html5 сослался на поток:
<source src="http://localhost:8081/stream.ogg">
Он мог транскодировать в vlc и прекрасно отображать в моем браузере Chrome и на моем телефоне с Android, но в итоге я выбрал другое решение, вместо того чтобы приступить к созданию собственного веб-приложения для размещения моей медиаколлекции и создания потоков для запрошенные файлы - я посмотрел и не смог найти бесплатный там, который сделал это так, как мне нужно / понравилось.