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

Комментарии (0)