Итак, выходим на финишную прямую. Стримить видео с андроида на VLC плеер мы уже научились, теперь осталось только интегрировать окошко с видео в JAVA приложение и начать рулить роботелегой.



В этом нам очень сильно поможет проект с открытым исходным кодом VLCJ CAPRICA.
The vlcj project provides a Java framework to allow an instance of a native VLC media player to be embedded in a Java application.
Идея у ребят простая, но гениальная (реально перцовая). Вместо мучений с библиотеками FFmpeg и прочим, надо сразу вызывать специалиста ядро нормального, функционального и профессионального медиаплеера VLC. И вызвать его прямо из JAVA приложения.

Кому интересно, просим под кат.

Поскольку, в этом вояже хватает подводных камней, то начнём, как водится, с очень простого и лишь затем перейдём к тривиальному.

Инсталляция пакета VLCJ


Первым делом проверьте установленную у вас версию медиапроигрывателя VLC. Свежая версия нам не нужна, там выпилено то, что требуется для udp стрима. Об этом уже говорилось в предыдущем посту. Поэтому качаем версию 2.2.6 Umbrella и заодно тщательно проверяем свой JAVA пакет. Они должны совпадать по разрядности. Если плеер использует 64-разрядную архитектуру, то и JDK обязан быть таким же. А то не взлетит.

После этого уже можно скачать сам пакет библиотек VLCJ



Обратите внимание, что нам нужен пакет vlcj-3.12.1 distribution (zip). Именно он работает с плеерами версий VLC 2.2.x. Разархивировать его можно куда угодно, главное, что не в папку самого VLC, ибо там по именам совпадают два файла. И если вы их перезапишите, кончится всё это полным провалом.

Далее, создаем проект в IDE IntelliJ IDEA (если у вас другое IDE, то ничем помочь не могу) и прописываем необходимые зависимости для интеграции библиотек VLCJ.



Делаем именно так для файлов:

jna-5.2.0

jna-platform-5.2.0

vlcj-3.12.1


Затем создаем единственный класс и пишем в нём следующую малюсенькую программку.

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import uk.co.caprica.vlcj.component.EmbeddedMediaPlayerComponent;
import uk.co.caprica.vlcj.discovery.NativeDiscovery;
import uk.co.caprica.vlcj.player.MediaPlayerFactory;
import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer;
import uk.co.caprica.vlcj.player.embedded.videosurface.CanvasVideoSurface;


public class BasicPlayer {
    public final JFrame frame;

    public static String mrl;

    public static MediaPlayerFactory mpf;

    public static EmbeddedMediaPlayer MediaPlayer;

    public static CanvasVideoSurface videoSurface;

    public static Canvas canvas;


    public static void main(final String[] args) {

        new NativeDiscovery().discover();

        mrl = "D:\\ttt.mp4";

        SwingUtilities.invokeLater(new Runnable() {

            @Override

            public void run() {

                BasicPlayer vp = new BasicPlayer();

                vp.start(mrl);
            }
        });
    }

    public BasicPlayer() {

        frame = new JFrame("My First Media Player");
        frame.setBounds(200, 100, 540, 340);
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.out.println(e);
                MediaPlayer.release();
                mpf.release();
                System.exit(0);
            }
        });

        JPanel contentPane = new JPanel();
        contentPane.setLayout(new BorderLayout());

        canvas = new Canvas();

        mpf = new MediaPlayerFactory();
        videoSurface = mpf.newVideoSurface(canvas);
        MediaPlayer = mpf.newEmbeddedMediaPlayer();
        MediaPlayer.setVideoSurface(videoSurface);

        contentPane.add(canvas, BorderLayout.CENTER); // вот тут добавляем медиакомпонент в графическую панель
        frame.setContentPane(contentPane);
        frame.setVisible(true);
    }

    public void start(String mrl) {

        
        MediaPlayer.playMedia(mrl);
    }
}

Да, пока мы пытаемся проигрывать просто файл (как видно из кода). С udp лучше не начинать — не заработает. А файл проигрывается вполне, если вы, конечно, не забыли его с соответствующим именем разместить заранее там, где надо. Думаю, что даже для самого начинающего джависта не составит труда разобраться в вышеприведенном коде.

Всё новое это:

вызов для VLCJ

  new NativeDiscovery().discover();

и создание самого инстанса медиаплеера

        mpf = new MediaPlayerFactory();
        videoSurface = mpf.newVideoSurface(canvas);
        MediaPlayer = mpf.newEmbeddedMediaPlayer();
        MediaPlayer.setVideoSurface(videoSurface);

А потом мы просто его добавляем в нужную графическую панель:

contentPane.add(canvas, BorderLayout.CENTER);

И всё, файл будет проигрываться именно в этом окошке.

А теперь попробуйте заменить

mrl = "D:\\ttt.mp4";

на

mrl = "udp://@:40002";

как мы спокойно делали в прошлом посте для стриминга видео через udp соединение.
Здесь такой номер не пройдёт. Окошко, конечно откроется, но покажет фигу, в смысле темный экран. Хотя никаких логов с ошибкой не будет. Просто не будет ничего.

Надо разобраться


Может быть не хватает кодека H264, который мы выбрали в настройках? Стоп, а как тогда только что проигрывался файл ttt.mp4? Он же не может проигрываться при такой настройке, он же — mp4.

Немедленно приходит понимание того, что библиотека VLCJ запускает только само ядро плеера. А какие там были предварительные настройки она не знает и знать не хочет. То есть, нам надо каким-то образом при запуске JAVA приложения, как-то передать VLC плееру, что мы хотим явно использовать кодек H264 или, допустим, хотим повернуть изображение или что-то ещё.

Оказывается, сделать это можно, используя класс MediaPlayerFactory. Только мы его запускали без аргументов, а можно даже с ними. На stackoverflow.com я тут же нашел простой пример, связанный с поворачиванием изображения:


 String[] args = {
  "--video-filter", 
  "rotate",
  "rotate-angle",
  "10"
};

 mpf = new MediaPlayerFactory(args);

То есть, чего-то там передаем строчным массивом в медиафабрику и оно там сохраняется для используемого медиаресурса.

Я попробовал этот способ для проигрывания файла и как водится, ничего не заработало. Оказывается, забыли две черточки добавить и разнесли по всему интернету. Пришлось догадываться, используя похожий метод transform.

Короче говоря, должно быть:

String[] args = {
"--video-filter",
"rotate",
"--rotate-angle",
"10"
};

Теперь наш эталонный файл перекривило как надо!



Дальше будет уже совсем просто:

Для определения кодека, согласно командной строке VLC мы добавляем в строковый массив строку:

"--demux=h264"

Снова пробуем udp канал


mrl = «udp://@:40002»;

И в этот раз всё работает, обозначая победу человеческого разума. Теперь это окошко с видео или несколько таких окошек вы сможете беспрепятственно портировать в графический интерфейс вашего JAVA приложения.

Казалось бы победа?


Не совсем. Легкое недоумение у меня вызвали временные задержки или по научному, лаги. Сначала они более менее приёмлимые, но если у вас хватит терпения просмотреть видео до конца, то вы увидите, что лаг к концу первой минуты трансляции достигает аж пяти секунд. У меня терпения хватило на 10 минут съемки, но, как ни странно, задержка больше не увеличивалась, а так и осталась в тех же пределах.

видео


Конечно, для просмотра видео с камеры такое сгодится, но для управления роботележкой едва ли. Даже луноход реагировал быстрее в два раза!

Подозрения сразу пали на процессы кэширования и они (подозрения )оказались верными.

Самым наглым оказался:

 caching  for network resources

Он как раз и отжирает по умолчанию практически всё, если ему вовремя не дать по рукам.
Может устроить лаг и:

caching  for cameras and microphones

Поэтому во избежание многосекундных задержек рекомендуется добавить во всё тот же строковый массив через запятую следующие строчки:

    "--live-caching=100", 
    "--network-caching=500",

Параметры там задаются в миллисекундах и поэтому каждый желающий может подобрать их под себя.

Ещё можно использовать ключ:

"--clock-jitter=time in milliseconds",

Тогда медиаплеер будет стараться оптимизировать джитер — подергивание экрана. Но там, чем больше установлено время, тем лучше оптимизируется и это понятно почему. Так что здесь остается лишь искать консенсус и видеть иногда в логах такое безобразие:



Вот хотел он, понимаешь, джиттер исправить, а ты временной промежуток слишком маленький поставил. Теперь сам виноват.

Теперь вроде бы все как надо. Задержку удалось сократить меньше, чем до одной секунды (правда, чуть-чуть меньше).

видео


В итоге, получился совсем крохотный рабочий код


import java.awt.*;

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import uk.co.caprica.vlcj.discovery.NativeDiscovery;
import uk.co.caprica.vlcj.player.MediaPlayerFactory;
import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer;
import uk.co.caprica.vlcj.player.embedded.videosurface.CanvasVideoSurface;


public class BasicVideoPlayer {
    public final JFrame frame;

    public static String mrl;

    public static MediaPlayerFactory mpf;

    public static EmbeddedMediaPlayer MediaPlayer;

    public static CanvasVideoSurface videoSurface;

    public static Canvas canvas;



    public static void main(final String[] args) {

        new NativeDiscovery().discover();


        mrl = "udp://@:40002";


        SwingUtilities.invokeLater(new Runnable() {

            @Override

            public void run() {

                BasicVideoPlayer vp = new BasicVideoPlayer();

                vp.start(mrl);


            }
        });
    }

    public BasicVideoPlayer() {

        frame = new JFrame("My First Media Player");
        frame.setBounds(200,100, 540, 340);
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                System.out.println(e);
                MediaPlayer.release();
                mpf.release();
                System.exit(0);
            }
        });

        JPanel contentPane = new JPanel();
        contentPane.setLayout(new BorderLayout());
        canvas = new Canvas();


        String[] args = {
               "--video-filter",
                "rotate",
                "--rotate-angle",
                "270",
                "--demux=h264",
                "--clock-jitter=100",
                "--live-caching=100",
                "--network-caching=500",
        };


        mpf = new MediaPlayerFactory(args);
        videoSurface = mpf.newVideoSurface(canvas);

        MediaPlayer = mpf.newEmbeddedMediaPlayer();
        MediaPlayer.setVideoSurface(videoSurface);


        contentPane.add(canvas, BorderLayout.CENTER);
        frame.setContentPane(contentPane);

        frame.setVisible(true);
    }

    public void start(String mrl) {
        
        MediaPlayer.playMedia(mrl);
    }
}

Теперь можно интегрировать видео трансляцию в мою программулину по управлению роботележкой, где раньше я передавал видео по кускам. И надо сказать код весьма упростился, а качество на порядок улучшилось. Плюс ко всему к видео потоку мы можем передать показания

акселерометров
гироскопов
уровня освещения
давления воздуха
показаний компаса
температуры
и даже влажности

При условии, конечно, что все эти сенсоры у вашего смартфона имеются.

И даже включить фару! Автоматически! Если уровень освещения упадёт.


Вряд ли кому особо интересно, но на случай ссылки на гитхаб:

для телеги
для смартфона

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


  1. Tarson Автор
    07.02.2020 22:10
    +1

    Написали с Гита
    Cool Project! I am looking for some examples that do live streaming with camera2 and MediaCodec api's (not the old 'render to texture' stuff) and your project is one of the few that is simple enough.
    I have plans to create a similar app like yours to allow streaming to FPV_VR_OS.
    However, for lower latency I think using the NDK is required (tough there is even less documentation/examples) available.

    Ерунда, а приятно…