Переключение звуковых дорожек в Flash с помощью RTMP сервера Wowza2

от автора

В данной статье описана древняя история о том, как мне удалось реализовать переключение звуковых дорожек для 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *