В данной статье описана древняя история о том, как мне удалось реализовать переключение звуковых дорожек для Flash-плеера с помощью RTMP сервера Wowza Media Server 2.
В далеком 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);
ссылка на оригинал статьи http://habrahabr.ru/post/263747/
Добавить комментарий