В данной статье описана древняя история о том, как мне удалось реализовать переключение звуковых дорожек для Flash-плеера с помощью RTMP сервера Wowza Media Server 2.
В далеком 2011 году я занимался исследованием возможностей стриминговых серверов для Adobe Flash Player'а. Передо мной стояла задача найти способ воспроизведения видео файлов с несколькими звуковыми дорожками. При этом было необходимо, чтобы переключение происходило без скачков по воспроизводящемуся видео. Поиск готовых решений в интернете никаких результатов тогда не дал. Более того, выяснилось, что сам Adobe Flash Player переключать дорожки не умеет и использует только первую попавшуюся…
Выручила меня реклама Adobe Flash Media Server'а. В примерах этого сервера был плеер с поддержкой адаптивного стриминга. Он умел незаметно переключать видео поток с одного битрейта на другой и обратно. Немного покопавшись, я обнаружил следующие подробности:
Я попробовал проделать этот трюк на файлах с одинаковым видео, но с разными аудио дорожками. Эксперимент удался успешно, переключая потоки, я слышал разные звуковые дорожки, при этом переход от одного видео файла к другому визуально был незаметен. Но радоваться было еще рано, так как вместе с N звуковыми дорожками приходится также хранить N копий видеоряда, а это слишком накладно.
Проанализировав данные, которые сервер отдает Flash-плееру по RTMP протоколу, я обнаружил, что аудио и видео потоки идут в отдельных друг от друга пакетах. При этом, лишние звуковые дорожки не передавались вовсе. То есть, выделением нужных дорожек из контейнера (demuxing) занимается сам RTMP сервер. Эта информация воодушевила меня, и я принялся подробнее изучать RTMP сервера с возможностью адаптивного стриминга. Одним из таких серверов оказался Wowza Media Server версии 2.
Отличительная особенность Wowza Media Server'а заключается в том, что он позволяет создавать классы для воспроизведения любых медиа файлов, для этого достаточно реализовать интерфейс IMediaReader и объявить свой класс в конфигурации сервера. Но вместо написания собственного декодера mp4 контейнера я принялся за реверс-инжиниринг классов сервера.
Декомпилировав классы MediaReaderH264 и QTMediaContainer из файла wms-mediareader-h264.jar, я обратил внимание на следующие строки:
Во-первых, очевидно, что MediaReaderH264 имеет доступ до moov-атома. Во-вторых, так как ссылка на контейнер является protected-полем, доступ можно получить наследуясь от этого класса.
Что же такое moov-атом? По спецификации контейнера mp4, атом moov содержит в себе всю информацию о частоте кадров, длине фильма, расположении кадров, конфигурации декодеров итп. Также он содержит в себе набор trak-атомов, которые описывают аудио и видео дорожки, а это как раз то, что нам нужно.
Декомпилировав класс QTAtommoov, можно увидеть следующую картину:
При попытке получить аудио дорожку, сервер идет по всем trak-атомам и выбирает первый попавшийся с типом smhd (sound media header). То есть, выбирается самая первая звуковая дорожка.
Чтобы проверить свои догадки, я решил сделал инъекцию в код библиотеки Wowza Media Server'а. Сначала я думал немного поправить декомпилированный код класса QTAtommoov, скомпилировать его обратно и просто заменить файл в jar-архиве. Но, к моему удивлению, все оказалось значительно проще. В исходниках серверного приложения я создал пакет com.wowza.wms.mediareader.h264.atom и поместил туда файл QTAtommoov.java со следующим содержанием:
Таки образом, была сделана небольшая модификация: вместо первой попавшейся аудио дорожки возвращалась вторая.
Скомпилировав и развернув сервер в таком виде, я был приятно удивлен тем, что внутри jar-библиотеки подцепился и работает мой класс, а Flash-плеер играет вторую звуковую дорожку в файле. Мне даже не пришлось пересобирать jar-библиотеку.
До финальной реализации прототипа переключения звуковых дорожек, оставалось лишь реализовать расширенный класс MediaReaderH264ext и объявить его в конфигурации сервера.
А для переключения звука, во Flash-плеере вызывался код:
В далеком 2011 году я занимался исследованием возможностей стриминговых серверов для Adobe Flash Player'а. Передо мной стояла задача найти способ воспроизведения видео файлов с несколькими звуковыми дорожками. При этом было необходимо, чтобы переключение происходило без скачков по воспроизводящемуся видео. Поиск готовых решений в интернете никаких результатов тогда не дал. Более того, выяснилось, что сам Adobe Flash Player переключать дорожки не умеет и использует только первую попавшуюся…
Выручила меня реклама Adobe Flash Media Server'а. В примерах этого сервера был плеер с поддержкой адаптивного стриминга. Он умел незаметно переключать видео поток с одного битрейта на другой и обратно. Немного покопавшись, я обнаружил следующие подробности:
- видео должно быть заранее закодировано в разных битрейтах;
- передача данных идет по протоколу RTMP;
- переключение качества идет по команде Flash приложения, с помощью функции NetStream.play2.
Я попробовал проделать этот трюк на файлах с одинаковым видео, но с разными аудио дорожками. Эксперимент удался успешно, переключая потоки, я слышал разные звуковые дорожки, при этом переход от одного видео файла к другому визуально был незаметен. Но радоваться было еще рано, так как вместе с N звуковыми дорожками приходится также хранить N копий видеоряда, а это слишком накладно.
Проанализировав данные, которые сервер отдает Flash-плееру по RTMP протоколу, я обнаружил, что аудио и видео потоки идут в отдельных друг от друга пакетах. При этом, лишние звуковые дорожки не передавались вовсе. То есть, выделением нужных дорожек из контейнера (demuxing) занимается сам RTMP сервер. Эта информация воодушевила меня, и я принялся подробнее изучать RTMP сервера с возможностью адаптивного стриминга. Одним из таких серверов оказался Wowza Media Server версии 2.
Отличительная особенность Wowza Media Server'а заключается в том, что он позволяет создавать классы для воспроизведения любых медиа файлов, для этого достаточно реализовать интерфейс IMediaReader и объявить свой класс в конфигурации сервера. Но вместо написания собственного декодера mp4 контейнера я принялся за реверс-инжиниринг классов сервера.
Декомпилировав классы MediaReaderH264 и QTMediaContainer из файла wms-mediareader-h264.jar, я обратил внимание на следующие строки:
// MediaReaderH264.class
import com.wowza.wms.mediareader.h264.atom.QTAtommoov;
import com.wowza.wms.mediareader.h264.atom.QTMediaContainer;
. . .
public class MediaReaderH264 implements IMediaReader
{
. . .
protected QTMediaContainer container;
}
// QTMediaContainer.class
public class QTMediaContainer extends QTAtom
{
public QTAtommoov getMoovAtom()
{
return moovAtom;
}
. . .
private QTAtommoov moovAtom;
}
Во-первых, очевидно, что MediaReaderH264 имеет доступ до moov-атома. Во-вторых, так как ссылка на контейнер является protected-полем, доступ можно получить наследуясь от этого класса.
Что же такое moov-атом? По спецификации контейнера mp4, атом moov содержит в себе всю информацию о частоте кадров, длине фильма, расположении кадров, конфигурации декодеров итп. Также он содержит в себе набор trak-атомов, которые описывают аудио и видео дорожки, а это как раз то, что нам нужно.
Декомпилировав класс QTAtommoov, можно увидеть следующую картину:
public class QTAtommoov extends QTAtom
{
public QTAtomtrak getTrackByMinf(String s)
{
QTAtomtrak qtatomtrak = null;
Iterator iterator = traks.iterator();
do
{
if(!iterator.hasNext())
break;
QTAtomtrak qtatomtrak1 = (QTAtomtrak)iterator.next();
if(qtatomtrak1 == null || !qtatomtrak1.getMinfType().equals(s))
continue;
qtatomtrak = qtatomtrak1;
break;
} while(true);
return qtatomtrak;
}
public QTAtomtrak getAudioTrack()
{
QTAtomtrak qtatomtrak = getTrackByMinf("smhd");
try
{
QTAtomstbl qtatomstbl = qtatomtrak != null ? qtatomtrak.getMdiaAtom().getMinfAtom().getStblAtom() : null;
if(!qtatomstbl.isValidAudioFormat())
qtatomtrak = null;
}
catch(Exception exception) { }
return qtatomtrak;
}
. . .
}
При попытке получить аудио дорожку, сервер идет по всем trak-атомам и выбирает первый попавшийся с типом smhd (sound media header). То есть, выбирается самая первая звуковая дорожка.
Чтобы проверить свои догадки, я решил сделал инъекцию в код библиотеки Wowza Media Server'а. Сначала я думал немного поправить декомпилированный код класса QTAtommoov, скомпилировать его обратно и просто заменить файл в jar-архиве. Но, к моему удивлению, все оказалось значительно проще. В исходниках серверного приложения я создал пакет com.wowza.wms.mediareader.h264.atom и поместил туда файл QTAtommoov.java со следующим содержанием:
public class QTAtommoov extends QTAtom
{
public int aTrackNum = 2;
. . .
public QTAtomtrak getTrackByMinf(String s, int count)
{
QTAtomtrak qtatomtrak = null;
Iterator iterator = traks.iterator();
do
{
if(!iterator.hasNext())
break;
QTAtomtrak qtatomtrak1 = (QTAtomtrak)iterator.next();
if(qtatomtrak1 == null || !qtatomtrak1.getMinfType().equals(s))
continue;
if (--count <= 0)
{
qtatomtrak = qtatomtrak1;
break;
}
} while(true);
return qtatomtrak;
}
public QTAtomtrak getTrackByMinf(String s)
{
return getTrackByMinf(s, 1);
}
public QTAtomtrak getAudioTrack()
{
QTAtomtrak qtatomtrak = getTrackByMinf("smhd", aTrackNum);
try
{
QTAtomstbl qtatomstbl = qtatomtrak != null ? qtatomtrak.getMdiaAtom().getMinfAtom().getStblAtom() : null;
if(!qtatomstbl.isValidAudioFormat())
qtatomtrak = null;
}
catch(Exception exception) { }
return qtatomtrak;
}
}
Таки образом, была сделана небольшая модификация: вместо первой попавшейся аудио дорожки возвращалась вторая.
Скомпилировав и развернув сервер в таком виде, я был приятно удивлен тем, что внутри jar-библиотеки подцепился и работает мой класс, а Flash-плеер играет вторую звуковую дорожку в файле. Мне даже не пришлось пересобирать jar-библиотеку.
До финальной реализации прототипа переключения звуковых дорожек, оставалось лишь реализовать расширенный класс MediaReaderH264ext и объявить его в конфигурации сервера.
public class MediaReaderH264ext extends MediaReaderH264 implements IMediaReader
{
private String filename;
private int aTrackNum;
private void init(String basePath, String mediaName)
{
HashMap<String, String> params = new HashMap<String, String>();
String[] query = mediaName.split(":", 2);
if (query.length > 1)
{
String[] args = query[1].split("&");
for (String arg : args)
{
String[] keyvalue = arg.split("=", 2);
params.put(keyvalue[0], keyvalue.length > 1 ? keyvalue[1] : "");
}
}
filename = query[0];
aTrackNum = "rus".equals(params.get("lang")) ? 2 : 1;
WMSLoggerFactory.getLogger(MediaReaderH264ext.class).info("filename: " + filename);
WMSLoggerFactory.getLogger(MediaReaderH264ext.class).info("aTrackNum: " + aTrackNum);
}
@Override
public void init(IApplicationInstance iapplicationinstance, IMediaStream imediastream, String ext, String basePath, String name)
{
WMSLoggerFactory.getLogger(MediaReaderH264ext.class).info("init: " + name);
this.init(basePath, name);
super.init(iapplicationinstance, imediastream, ext, basePath, filename);
}
@Override
public void open(String basePath, String name)
{
WMSLoggerFactory.getLogger(MediaReaderH264ext.class).info("open: " + name);
super.open(basePath, name);
if (container != null && container.getMoovAtom() != null)
container.getMoovAtom().aTrackNum = this.aTrackNum;
}
}
А для переключения звука, во Flash-плеере вызывался код:
var opt = new NetStreamPlayOptions();
opt.transition = NetStreamPlayTransitions.SWITCH;
opt.streamName = "mp4e:video.mp4:lang=rus";
ns.play2(opt);