Как стать автором
Обновить

Стрим видео с Android устройства по UDP в JAVA приложение

Время на прочтение7 мин
Количество просмотров9.9K
Итак, выходим на финишную прямую. Стримить видео с андроида на 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);
    }
}

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

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

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

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


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

для телеги
для смартфона
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+12
Комментарии1

Публикации

Истории

Работа

Java разработчик
358 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн