29 мая 2012 в 10:43

Android + Arduino + 4 колеса. Часть 3 – передача видео и звука

Наконец, продвижение вперёд. Всё, теперь домашние не посмеют назвать робота Митю «радиоуправляемой машинкой»!

Оказалось непросто найти относительно лёгкий и работающий способ передачи видео и аудио потоков от Android-гаджета к удалённому управляющему приложению на ПК. Без этого шага я категорически не хотел двигаться дальше, поэтому довольно надолго завяз в своём упрямстве.


Большое спасибо всем кто мне помогал, без помощи я бы эту задачку не осилил. Переписка с единомышленниками свела меня с очень приятными и интересными людьми, эту сторону хобби я как-то не рассматривал раньше. Оказалось робот Митя помог мне найти близких по духу людей. И это был совершенно неожиданный для меня бонус. Возможно, в этом и заключается главный «профит» хобби? Только сейчас осознал, что общение мне важнее, чем просто «рукоделие». Зачем это всё надо, если не с кем поделиться, посоветоваться, похвастаться?

Буду закругляться со столь любимой лирической составляющей. Эта статья продолжение истории про робота Митю. В первой части я рассказал как строил своего робота, во второй – описал что понадобится, чтобы его программировать. В этой статье сконцентрируюсь на вопросе передачи видео и звука от робота к оператору. Не стану здесь повторяться о проекте в целом и его программной архитектуре в частности – эти вопросы подробно описаны в первой части.

Самый волнующий вопрос: каков результат, насколько будет притормаживать картинка и звук? Вот мой видеоответ:



Прошу прощения за наводки, но мне обязательно нужно было продемонстрировать как исходный звук, так и звук, воспроизведённый на ПК. Задержка, конечно, есть. Причём, при воспроизведении звук немного отстаёт от картинки. Видео отстаёт от реальности совсем незначительно (всё-таки надо принимать во внимание, что это не аналоговая система, и всем заправляет пусть мощный, но телефон). Я пробовал управлять роботом, и дискомфорта с такой задержкой видео не испытывал. Так что меня результат вполне устроил – с небольшим отставанием звука я вполне готов смириться.

Качество выводимой оператору картинки соответствует тому, на что способна фронтальная камера на HTC Sensation. Я мог бы воспользоваться и основной камерой, но тогда пришлось бы отказаться от мимики Мити, а на это я пойти не могу.

Для простоты навигации приведу план дальнейшего содержания статьи:
1. Эволюция решения (только для любопытных)
2. IP Webcam
3. Воспроизведение видео в Windows-приложении
4. Воспроизведение звука в Windows-приложении
5. Итог

Эволюция решения (только для любопытных)


Чтобы меня не обвинили в «многобуквстве» предлагаю компромисс: люди любопытные, или сочувствующие проекту, или просто сердобольные (не зря же я всё это писал) могут почитать как я выкручивался и какие решения находил и отметал.

Если серьёзно, то я хочу поделиться по-возможности всем приобретённым опытом, потому что уверен, что ценность представляет не только положительный опыт, но и отрицательный. А вдруг есть люди, которые умеют учиться на ошибках других? Кроме того, я был бы очень признателен, если бы кто-нибудь нашёл или показал выход из встреченных мной тупиков. И тогда это был бы уже обмен опытом.

В этом разделе я пишу про тупики. Если Вы просто ищите рецепт передачи видео и звука от робота на ПК, можете переходить к следующему разделу.

Замысел был прост: на уровень Android-приложения встроить код, реализующий трансляцию видео и аудио потоков, а на уровень управляющего Windows-приложения встроить код, принимающий и воспроизводящий эти потоки.

Всевозможные «наколеночные» решения с применением готовых, не интегрируемых в мой код продуктов я отмёл сразу – некрасиво.

Начал я с видео. И почему-то был глубоко убеждён, что проблем с формированием и трансляцией видеопотока не будет. XXI век всё-таки, смартфоны у кого-то уже четырёхядерные… Ан нет. Несмотря на то, что у каждого телефона на борту по две камеры, сделать из телефона веб-камеру не так-то просто. Удивительно, но встроенных в Android готовых программных средств для решения этой задачи нет. Не знаю что является причиной, так как для производителей телефонов особых технических проблем эта задача не представляет, но подозреваю, всё объясняется сложностью сертификации в некоторых странах аппаратов с таким ПО. Вдруг это уже шпионское оборудование? Это единственное объяснение, которое я смог придумать. Но Митя так звенит моторами, что лазутчик из него, как из бегемота воздушный шарик. Поэтому совесть с бессонницей меня не мучают.

Перекапывая google, code.google, stackoverflow, android developers и всех-всех-всех я нашёл несколько очень интересных, но никуда меня не приведших, решений. И на них я потратил уйму времени. На всякий случай опишу их и почему я отказался от каждого из них. Опыт может оказаться полезен. Для каких-то задач эти решения вполне пригодны, но я с ними не подружился. Совсем тупиковые варианты я опущу. Оставлю только те, у которых есть шанс.

Вариант первый: использование класса MediaRecorder, входящего в состав Android. Я было подумал, что вот оно, решение всех моих проблем – тут и видео, и аудио. На выходе получу поток 3gp, передам его по UDP на ПК. Но увы. MediaRecorder так просто не станет работать с потоками – он только с файлами умеет. Погуглив, на форуме groups.google.com я нашёл очень интересную дискуссию. Здесь обсуждалась похожая задача, и в ходе её обсуждения всплыла ссылка на ещё один интересный пост. Там описывается этакий «хак», как обмануть MediaRecorder, чтобы он думал, что работает с файлом, а на самом деле подсунуть ему для записи поток вывода в сокет. Повторять описание реализации я не буду, там всё написано. Участников дискуссии этот вариант вполне устроил. Задача стояла записать видео с телефона в файл на удалённом компьютере, минуя карту памяти телефона. Именно в файл – изучая этот вариант глубже, я убедился, что для веб-трансляции он не подходит. Дело в том, что MediaRecorder на моём аппарате работает следующим образом: при старте видеозаписи создаётся файл и в его головной части резервируется место для записи размера потока. Это поле заполняется только по завершении видеозаписи. А для этого выходной поток MediaPlayer-а должен поддерживать позиционирование (операция Seek). Файловый поток, например, на такое способен, а при широковещательной трансляции по сети поток, естественно, строго последовательный и прыгнуть в его начало, чтобы заполнить поле с окончательным размером не получится. Но у ребят задача стояла писать в файл на удалённом компьютере. Поэтому они сначала «сливали» свой поток в файл, а затем открывали этот файл на запись и вписывали в нужное место его размер. После этого такой файл уже можно было проигрывать чем угодно.

Задача в моём проекте другая: у меня нет момента завершения записи. И я не могу определить размер видеопотока. Т.е. выходной поток приложения MediaPlayer не предназначен для веб-трансляции. По крайней мере на моём аппарате. Этот вариант пришлось выбросить.

Ладно, я решил попробовать всё сделать сам. Android предоставляет возможность получить «сырые» кадры от камеры устройства. Эта функция везде замечательно описана, интернет заполнен примерами преобразования полученного потока с кадром от камеры устройства в jpeg-файл и сохранения этого файла на карту памяти. В моём случае, поток jpeg-кадров можно было гнать по UDP на ПК. Но как быть, если поток будет битый (всё-таки это UDP)? Надо как-то разделять кадры метками или использовать для этого заголовок jpeg. На ПК кадры придётся как-то вычленять из потока. Всё это как-то низкоуровнево, и поэтому мне совсем не нравилось. Определённо надо использовать уже готовый кодек. А ещё было бы здорово использовать какой-нибудь стандартный медийный протокол. И я начал собирать информацию в этом направлении. Так второй вариант отпал не родившись и появился третий: использовать ffmpeg.

ffmpeg заслуженно самый популярный и раскрученный продукт в данной области. Благодаря развитому сообществу на него вполне можно положиться, и я решил, что с направлением дальнейших работ я определился. И тут я подвис месяца на два. Оказывается, ffmpeg под Android придётся компилировать самому. Заодно придётся познакомиться с Android NDK, так как ffmpeg написан на C. Сборка ffmpeg под Android крайне затруднительна в ОС Windows. «Затруднительна», это не совсем искренне, на самом деле я не встретил ни одного упоминания об успешной компиляции в Windows. Вопросов на эту тему масса, но на всех форумах в один голос рекомендуют развернуть Linux. Сейчас, возможно, ситуация изменилась: вот вопрос к статье у меня в блоге и мой ответ. Никакого опыта использования Linux у меня не было. Но меня разбирало любопытство, что за зверь такой Linux, пришлось поставить Ubuntu. Дальше выяснилось, что актуальной информации по компиляции ffmpeg в Интернете нет. Изрядно намучившись, сумел-таки собрать заветный ffmpeg. Процесс сборки я довольно подробно описал у себя в блоге.

К этому моменту Митя давно пылился в углу брошенный и забытый. А у меня впереди маячили только новые и совершенно безрадостные сражения с Android NDK, полным отсутствием документации к API ffmpeg, погружение в таинства кодеков и компиляция ffserver (это потоковый видеосервер, часть проекта ffmpeg). С помощью ffserver можно организовать трансляцию видео и аудио от робота на любой ПК в сети. Друзья уже думали, что с робототехникой я перегорел, а я плакал, кололся, но продолжал грызть гранит ffmpeg-а. Не уверен, что я смог бы пройти этот путь до конца.

И тут ко мне подоспела помощь. Помните я говорил о замечательных единомышленниках, с которыми меня свело общее хобби? Должен сказать, что робот Митя к этому моменту был не одинок. В другом городе у него уже появился внешне очень похожий, но внутренне кое-где отличающийся и местами даже в лучшую сторону, брат-близнец. Я переписываюсь с Luke_Skypewalker – автором этого робота. В очередном письме от него вижу совет посмотреть приложение под Android, называющееся IP Webcam. А ещё я узнаю, что это приложение транслирует по HTTP видео и звук с Android-устройства, имеет API и умеет работать в фоновом режиме!

Надо признаться, что после публикации первой части статьи про робота Митю, на сайте проекта я получил комментарий от пользователя kib.demon с советом присмотреться к IP Webcam. Я посмотрел, но плохо и не увидел главного – того, что у этого приложения есть программный API. Я подумал, что приложение, реализующее веб-камеру само по себе мне не нужно и быстро забыл о его существовании. А ещё был комментарий к самой статье. Там не было сказано про API, и я его я тоже проморгал. Эта ошибка стоила Мите трёх месяцев в углу.

Не думаю, что я зря столько копался с ffmpeg. После своих постов (был ещё английский вариант) мне присылают много вопросов, видимо, принимая меня за знатока в этой области. Я стараюсь отвечать в меру своих познаний, но большая часть вопросов так и висит в воздухе. Тема по-прежнему актуальна. Я был бы очень рад, если бы кто-нибудь продолжил эту работу и описал её. Всё-таки в интернете почти ничего нет о программном взаимодействии с ffmpeg. А что есть, устарело на несколько лет. Благодаря использованию приличных кодеков ffmpeg может позволить выжать из минимума трафика максимум видео. Для многих проектов это будет очень полезно.

IP Webcam


Итак, с уровнем Android-части робота Мити я определился: для трансляции видео и аудио будет использоваться приложение IP Webcam.
На сайте разработчика есть страничка, описывающая функции, доступные в API. Там же можно скачать исходники крошечного Android-проекта, демонстрирующего как программно взаимодействовать с IP Webcam.

Собственно, вот ключевая часть этого примера:

Intent launcher = new Intent().setAction(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
Intent ipwebcam = 
	new Intent()
	.setClassName("com.pas.webcam", "com.pas.webcam.Rolling")
	.putExtra("cheats", new String[] { 
			"set(Photo,1024,768)",         // set photo resolution to 1024x768
			"set(DisableVideo,true)",      // Disable video streaming (only photo and immediate photo)
			"reset(Port)",                 // Use default port 8080
			"set(HtmlPath,/sdcard/html/)", // Override server pages with ones in this directory 
			})
	.putExtra("hidebtn1", true)                // Hide help button
	.putExtra("caption2", "Run in background") // Change caption on "Actions..."
	.putExtra("intent2", launcher)             // And give button another purpose
    .putExtra("returnto", new Intent().setClassName(ApiTest.this,ApiTest.class.getName())); // Set activity to return to
startActivity(ipwebcam);

Замечательно то, что можно управлять ориентацией транслируемого видео, его разрешением, качеством картинки. Можно переключаться между задней и фронтальной камерами, включать выключать светодиоды вспышки. Всем этим можно управлять в настройках перед запуском трансляции или программно, используя приведённые автором проекта чит-коды, как
это показано в примере.

Чуть отвлекусь: к сожалению со вспышкой (фара Мити) у меня прокол. К IP Webcam это никак не относится. Я уже столкнулся с этой проблемой в своём коде ранее. На моём аппарате если активировать фронтальную камеру, управлять вспышкой уже нельзя. Подозреваю, это не только в HTC Sensation.

В приложении IP Webcam после запуска трансляции видео на экране начинает отображаться видео с активной камеры. Поверх видео предусмотрено отображение двух настраиваемых кнопок. По умолчанию одна инициирует диалог со справкой, а вторая открывает меню доступных действий. В примере автор первую кнопку прячет, а вторую настраивает на перевод работы приложения в фоновом режиме.

Проверить аудио/видео трансляцию можно, например, в браузере или в приложении VLC media player. После запуска трансляции IP Webcam в браузере по адресу http://<IP телефона>:<порт> становится доступен «Сервис камеры смартфона». Здесь можно просмотреть отдельные кадры, включить воспроизведение видео и звука. Для просмотра видеопотока, например, в VLC media player необходимо открыть URL http://<IP телефона>:<порт>/videofeed. Для воспроизведения звука доступны два URL: http://<IP телефона>:<порт>/audio.wav и http://<IP телефона>:<порт>/audio.ogg. Есть существенное запаздывание звука, но как выяснилось позже, связано это с кэшированием при воспроизведении. В VLC media player можно поставить кэширование на 20 мСек и запаздывание звука станет несущественным.

Это были хорошие новости, а теперь плохие: я, конечно, нашёл трудности. На моём аппарате в фоновом режиме IP Webcam прекращал трансляцию видео. Я почитал комментарии в Google Play и обнаружил, что такое происходит у всех счастливчиков с 4-ым Android-ом. Про 3-ий не знаю. А незадолго до «открытия» IP Webcam я обновил версию Android с 2.3.4 до 4.0.3. Невозможность фоновой работы была фатальна, потому что я не мог оставить работающей активити, транслирующую видео, поверх открыв своё активити с мордочкой Мити, да ещё и управляющее им. Активация мордочки переводила активити IP Webcam в фоновый режим и трансляция прекращалась. Да, ту часть приложения, которая управляла роботом, я мог организовать в виде сервиса, но как быть с лицом Мити?

Не найдя выхода самостоятельно, я решился написать письмо автору проекта IP Webcam. Автор «наш», в смысле писать можно по-русски. Меня беспокоило три вещи:

  1. Прекращение трансляции в фоновом режиме.
  2. Отставание звука от картинки.
  3. Некорректно отображался IP-адрес телефона в IP Webcam. Это не критично, но поначалу сбивало с толку. Хотя это ерунда, здесь вопрос только в некорректном тексте на экране телефона.

Должен сказать огромное спасибо автору IP Webcam (Павел Хлебович). Ещё одно замечательное знакомство. В ходе переписки, которая у нас завязалась, он не только ответил на все мои вопросы, но и прислал ещё один демо-проект, в котором показано, как можно выкрутиться с фоновым режимом и моим Android 4.0.3.

Больше всего, конечно, меня волновали первые два вопроса. По второму Павел всё рассказал мне про кэширование звука в браузере и VLC media player.

А вот что он написал мне по первому вопросу: «Похоже, что все телефоны на 4.x уже используют драйвер V4L, который не позволяет захватывать видео без поверхности для его показа. Поэтому в фоновом режиме видео и не работает. Как обходное решение можно попробовать поверх моей Activity сделать свою, описать её как полупрозрачную, чтобы поверхность IP Webcam не уничтожалась, а на самом деле сделать её непрозрачной и показывать что надо».

Вроде звучит понятно, но как можно открыть одно активити над другим и чтобы оба работали? Я думал только одно активити может быть активно в единицу времени (извините за каламбур). Провёл несколько экспериментов, но у «нижнего» активити всегда срабатывало onPause. Павел опять помог: специально сделал для меня демо-проект. Удивительно, но идея с расположением одного активити поверх другого работает! Демо-проект Павла я чуть-чуть доработал (косметически) и выложил на сайт робота Мити.

Опишу что сделано в этой демонстрации.

1. В layout главной activity добавлена кнопка Button1.
2. Добавлен ещё один layout «imageoverlay.xml» с картинкой «some_picture.png». Больше в нём ничего нет.
3. Добавлена OverlayActivity.java, отображающая контент imageoverlay.xml. Именно эта активити будет отображаться поверх видео IP Webcam. В моём основном проекте на этом месте будет активити, управляющая роботом. Она же лицо Мити.
4. В манифест должны быть добавлены:

а) Описание OverlayActivity:

<activity android:name="OverlayActivity" android:launchMode="singleInstance" android:theme="@android:style/Theme.Dialog">
</activity>

Обязательно должны быть заполнены атрибуты launchMode и theme указанными значениями.

б) Права для обращения к камере устройства:

<uses-permission android:name="android.permission.CAMERA"/>

5. Дополнить MainActivity.java:

package ru.ipwebcam.android4.demo;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends Activity {
	static String TAG = "IP Webcam demo";
	Handler h = new Handler();

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        final Button videoButton = (Button)findViewById(R.id.button1);
        
        videoButton.setOnClickListener(new OnClickListener() {
			public void onClick(View v) {
	    		Intent launcher = new Intent().setAction(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
	    		Intent ipwebcam = 
						new Intent()
						.setClassName("com.pas.webcam", "com.pas.webcam.Rolling")
						.putExtra("hidebtn1", true)                // Hide help button
						.putExtra("caption2", "Run in background") // Change caption on "Actions..."
						.putExtra("intent2", launcher);            // And give button another purpose
	    		
	    		h.postDelayed(new Runnable() {
					public void run() {
						startActivity(new Intent(MainActivity.this, OverlayActivity.class));
			    		Log.i(TAG, "OverlayActivity started");
					}
				}, 4000);
	    		
	    		startActivityForResult(ipwebcam, 1);
	    		
	    		h.postDelayed(new Runnable() {
					public void run() {
						sendBroadcast(new Intent("com.pas.webcam.CONTROL").putExtra("action", "stop"));
			    		Log.i(TAG, "Video is stopped");
					}
				}, 20000);
	    		
	    		Log.i(TAG, "Video is started");
			}
		});
    }
}

По нажатию на кнопку запускается IP Webcam, затем через 4 секунды поверх него открывается наше активити с картинкой. Размер изображения специально сделан меньше разрешения экрана (по крайней мере для HTC Sensation), чтобы было видно оба активити. А через 20 секунд после запуска, работа IP Webcam на заднем фоне прекращается, но наше «основное» активити продолжает работать.

Вот как это выглядит на телефоне:

Я и Робот Митя

Дальше мне оставалось совсем незначительно видоизменить Android-приложение Мити, и я уже мог видеть его камерой и слышать его микрофоном в браузере на ПК.

Воспроизведение видео в Windows-приложении


Не задумывался об этом раньше, но только на этом этапе понял, что видео от IP Webcam не совсем видео. Это MJPEG. Т.е. транслируемый поток представляет собой последовательную передачу кадров в JPEG-формате. Я совсем слаб в вопросах, связанных с передачей видео, поэтому не ожидал прыти от MJPEG. И зря – результат меня вполне устроил.

Для воспроизведения MJPEG-потока я нашёл замечательный бесплатный продукт MJPEG Decoder. Поиск средства для отображения MJPEG осложнялся тем, что управляющее Митей Windows-приложение использует фреймворк XNA. MJPEG Decoder поддерживает XNA 4.0, а ещё он работает с WinForms, WPF, Silverlight и Windows Phone 7 приложениями.

Исходники Мити по-прежнему открыты, но там много всего, поэтому здесь я приведу свой демо-пример использования MJPEG Decoder в XNA-приложении для воспроизведения видеопотока от IP Webcam. А в следующем разделе я дополню этот пример кодом для воспроизведения звука от IP Webcam.

Итак, создаём Windows Game (4.0) проект. В ссылки этого проекта добавляем библиотеку «MjpegProcessorXna4» (скачиваем её с сайта проекта или у меня).

В файле Game1.cs объявляем использование пространства имён MjpegProcessor:

using MjpegProcessor;

Объявляем закрытое поле mjpeg:

private MjpegDecoder mjpeg;

Объявляем текстуру для вывода видео:

private Texture2D videoTexture;

Дополняем метод Initialize:

this.mjpeg = new MjpegDecoder();

Дополняем метод Update:
1. По нажатию на пробел запускаем воспроизведение видеопотока:

this.mjpeg.ParseStream(new Uri(@"http://192.168.1.40/videofeed"));

2. По нажатию на Esc останавливаем воспроизведение:

this.mjpeg.StopStream();

3. Принимаем очередной кадр при вызове Update:

this.videoTexture = this.mjpeg.GetMjpegFrame(this.GraphicsDevice);

Дополняем метод Draw:

if (this.videoTexture != null)
{
    this.spriteBatch.Begin();
    Rectangle rectangle = new Rectangle(
    0,
    0,
    this.graphics.PreferredBackBufferWidth,
    this.graphics.PreferredBackBufferHeight);
    this.spriteBatch.Draw(this.videoTexture, rectangle, Color.White);
    this.spriteBatch.End();
}

Вот почти и всё. Почти, потому что всё уже работает, но кадры при воспроизведении сменяюся очень редко. У меня раз в 1-2 секунды. Чтобы это исправить, осталось подправить конструктор класса следующим образом:

public Game1()
{
    this.IsFixedTimeStep = false;
    this.graphics = new GraphicsDeviceManager(this);
    this.graphics.SynchronizeWithVerticalRetrace = false;
    Content.RootDirectory = "Content";
}

Содержимое файла Game1.cs я приведу в конце следующего раздела, когда дополню его кодом воспроизведения потокового звука.

Воспроизведение звука в Windows-приложении


Мне не удалось найти способ, как с помощью стандартных средств XNA или даже .NET можно воспроизвести аудиопотоки от IP Webcam. Ни «audio.wav», ни «audio.ogg». Я находил примеры, где потоковое аудио проигрывалось с помощью статического класса MediaPlayer из XNA фпеймворка, но наши потоки оказались ему не по зубам.

Очень много ссылок в Сети на открытые проекты NAUDIO и SlimDX. Но в первом я запустил демо-приложение и оно тоже не справилось, а второй я не осилил. Скатываться до уровня DirectX очень не хотелось. Совершенно случайно я нашёл замечательный выход. оказывается, вместе с всё тем же самым замечательным VLC media player поставляется ActiveX-библиотека! Т.е. под Windows я могу работать с VLC используя COM-интерфейсы. Описание интерфейсов очень скупое, Wiki проекта VLC содержит вообще устаревшую информацию. Неприятно, но всё удалось.

Для работы с интерфейсами ActiveX библиотеки её надо сначала зарегистрировать. При инсталляции VLC media player она не регистрируется и, как я понял, лежит мёртвым грузом в папке Vlc. Для этого запустите консоль (cmd.exe) и наберите:

regsvr32 "c:\Program Files (x86)\VideoLAN\VLC\axvlc.dll"

Путь, конечно, уточните. Если у вас Windows Vista или Windows 7, консоль запускайте от имени администратора.

Теперь звук от IP Webcam мы можем воспроизводить откуда угодно. Тренировался я на Excel. Можно и на VBScript, но в VBA члены классов вылезают в подсказках. Если есть желание, вот быстрый пример. Откройте Excel, нажмите Alt+F11. В окне Microsoft Visual Basic for Applications меню Tools -> References… Подключите ссылку на «VideoLAN VLC ActiveX Plugin». Теперь можно добавить модуль и вписать туда текст макроса:

Sub TestAxLib()
    Dim vlc As New AXVLC.VLCPlugin2
        
    vlc.Visible = False
    vlc.playlist.items.Clear
    vlc.AutoPlay = True
    vlc.Volume = 200
    vlc.playlist.Add "http://192.168.1.40:8080/audio.wav", Null, Array(":network-caching=5")
    vlc.playlist.playItem (0)
    
    MsgBox "Hello world!"
    
    vlc.playlist.stop
End Sub

Стартуйте IP Webcam, запускайте макрос – будет звук! Кстати, так же можно сделать и воспроизведение видео.

Возвращаясь к нашему тестовому XNA-приложению, для воспроизведения потокового звука надо сделать следующее:

Добавить в проект ссылку на COM-компоненту «VideoLAN VLC ActiveX Plugin». В «Обозревателе решений» она появится как «AXVLC». Выделить ссылку «AXVLC» и в окне «Свойства» установить свойство «Внедрить типы взаимодействия» в значение False.

Объявить использование пространства имён AXVLC:

using AXVLC;

Объявить объект:

private AXVLC.VLCPlugin2 audio;

Дополнить метод Initialize:

this.audio = new AXVLC.VLCPlugin2Class();

Дополнить метод Update:
1. По нажатию на пробел запускаем воспроизведение аудиопотока:

this.audio.Visible = false;
this.audio.playlist.items.clear();
this.audio.AutoPlay = true;
this.audio.Volume = 200;
string[] options = new string[] { @":network-caching=20" };
this.audio.playlist.add(
    @"http://192.168.1.40:8080/audio.wav",
    null,
    options);
this.audio.playlist.playItem(0);

2. По нажатию на Esc останавливаем воспроизведение аудиопотока:

if (this.audio.playlist.isPlaying)
{
    this.audio.playlist.stop();
}

Итак, вот что должно получиться:

using System;
using System.Collections.Generic;
using System.Linq;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

using AXVLC;
using MjpegProcessor;
    
namespace WindowsGame1
{
    /// <summary>
    /// Главный игровой класс.
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        /// <summary>
        /// Текстура для вывода видео.
        /// </summary>
        private Texture2D videoTexture;

        /// <summary>
        /// Декодер MJPEG.
        /// Используется для воспроизведения потокового видео (точнее, MJPEG), полученного от IP Webcam.
        /// </summary>
        /// <remarks>
        /// Требует подключения .NET библиотеки "MjpegProcessorXna4".
        /// </remarks>
        private MjpegDecoder mjpeg;

        /// <summary>
        /// Плагин VLC.
        /// Используется для воспроизведения потокового аудио, полученного от IP Webcam.
        /// </summary>
        /// <remarks>
        /// Объект из ActiveX-библиотеки VLC (www.videolan.org).
        /// Требует установки VLC, регистрации ActiveX-библиотеки axvlc.dll, а затем добавления в ссылки проекта COM-компоненты "VideoLAN VLC ActiveX Plugin" (в обозревателе решений отображается как "AXVLC").
        /// </remarks>
        private AXVLC.VLCPlugin2 audio;

        /// <summary>
        /// Конструктор класса.
        /// </summary>
        public Game1()
        {
            this.IsFixedTimeStep = false;
            this.graphics = new GraphicsDeviceManager(this);
            this.graphics.SynchronizeWithVerticalRetrace = false;
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Инициализация игры перед запуском.
        /// </summary>
        protected override void Initialize()
        {
            base.Initialize();

            // Создание экземпляра декодера:
            this.mjpeg = new MjpegDecoder();

            // Создание COM-объекта для воспроизведения звука:
            this.audio = new AXVLC.VLCPlugin2Class();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // По нажатию на пробел будет запущено воспроизведение видео и аудио:
            if (Keyboard.GetState().IsKeyDown(Keys.Space))
            {
                // Запуск воспроизведения видео:
                this.mjpeg.ParseStream(new Uri(@"http://192.168.1.40:8080/videofeed"));

                // Запуск воспроизведения аудио:
                this.audio.Visible = false;
                this.audio.playlist.items.clear();
                this.audio.AutoPlay = true;
                this.audio.Volume = 200;
                string[] options = new string[] { @":network-caching=20" };
                this.audio.playlist.add(
                    @"http://192.168.1.40:8080/audio.wav",
                    null,
                    options);
                this.audio.playlist.playItem(0);
            }

            // По нажатию на Esc воспроизведение видео останавливается:
            if (Keyboard.GetState().IsKeyDown(Keys.Escape))
            {
                // Прекращение воспроизведения видео:
                this.mjpeg.StopStream();

                // Прекращение воспроизведения аудио:
                if (this.audio.playlist.isPlaying)
                {
                    this.audio.playlist.stop();
                }
            }

            // Получение кадра:
            this.videoTexture = this.mjpeg.GetMjpegFrame(this.GraphicsDevice);

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            if (this.videoTexture != null)
            {
                this.spriteBatch.Begin();
                Rectangle rectangle = new Rectangle(
                    0,
                    0,
                    this.graphics.PreferredBackBufferWidth,
                    this.graphics.PreferredBackBufferHeight);
                this.spriteBatch.Draw(this.videoTexture, rectangle, Color.White);
                this.spriteBatch.End();
            }
            
            base.Draw(gameTime);
        }
    }
}


Итог


Закрыв этот этап, у меня появились как технические, так и нетехнические выводы.

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

Митя всё видит и слышит, но ничего не может сказать. Для начала надо расширить его мимику и обучить каким-нибудь базовым жестам, типа, «да», «нет», «повилять хвостом». Дальше надо будет делать воспроизведение звука в обратном направлении – от оператора к роботу.

Ещё один объявившийся вопрос – постоянная гудяще-жужжащая работа вертикального сервопривода, управляющего головой Мити. Из-за веса телефона сервопривод вынужден поддерживать заданный угол и постоянно гудит и мелко вибрирует. На видео это не сказывается, а вот микрофон ощутимо забивает. Оператор, в результате, ничего кроме жужжания не слышит. В некоторых положениях головы звук прекращается и тогда всё прекрасно слышно. Пока даже не знаю как решать эту проблему.

Мои нетехнический опыт, это то, что я открываю новые стороны своего хобби. Стоя в углу, робот Митя как-будто живёт своей жизнью: он знакомит меня с интересными и в чём-то очень похожими на меня людьми, он умудряется находить мне предложения о работе и каждый вечер я вприпрыжку спешу домой, чтобы успеть сделать что-нибудь ещё в этом проекте. Надеюсь, тут нет психиаторов… Словом, такой отдачи от вечернего времяпрепровождения (и с моей стороны, и с Митиной) я никак не ожидал.

И вот ещё вывод, который я постараюсь запомнить. Я прекрасно понимаю, ничто так не мотивирует, как победы. И ничто так не убивает интерес, как долгое отсутствие побед. Думаю не все обладают такой степенью упёртого занудства, как я. А даже я был на грани, чтобы всё это не бросить. Я говорил себе, что займусь пока другим проектом, отдохну от этого, а потом обязательно вернусь. Но в глубине души я знал что это значит и старался не смотреть в Тот Угол. Решение задачи передачи видео было для меня большим шагом в проекте, и я постараюсь больше не делать больших шагов. Большой шаг это большой риск. Я чуть не взялся за другую работу и она казалась мне более привлекательной, а сейчас я о ней даже не вспоминаю. В голове у меня роятся планы на Митю. Так что теперь буду ставить перед собой задачи поменьше. Я хочу много побед, это корм для моего удовольствия! Постараюсь в будущем обойтись без больших побед. Я выбираю много-много маленьких.
Дзахов Дмитрий @DmitryDzz
карма
37,0
рейтинг 0,0
Похожие публикации
Самое читаемое

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

  • +4
    Насчёт вертикальной сервы — делайте противовес. Дёшево и сердито.
    • 0
      Вариант! И не надо городить шаговых двигателей, или мощных серв.
      • +1
        Т.к. инерцию никто не отменял(а с противовесом она несколько увеличится), совсем слабые сервы ставить тоже не стоит.
      • +1
        Дмитрий, приветствую. Я эту проблему решил так
  • 0
    Червячная передача не имеет обратного хода. То есть установив камеру в определенное положение не нужно будет его в дальнейшем поддерживать.
  • 0
    Попробуйте для vlc подкрутить :http-caching для уменьшения задержки.
    • 0
      Проверил на практике, прямо приведённым в статье примером на VBA. Я попробовал 4 варианта.

      Sub TestAxLib()
          Dim vlc As New AXVLC.VLCPlugin2
              
          vlc.Visible = False
          vlc.playlist.items.Clear
          vlc.AutoPlay = True
          vlc.Volume = 200
          vlc.playlist.Add "http://192.168.1.40:8080/audio.wav", Null, Array(":network-caching=5")
          'vlc.playlist.Add "http://192.168.1.40:8080/audio.wav", Null, Array(":network-caching=5", ":http-caching=5")
          'vlc.playlist.Add "http://192.168.1.40:8080/audio.wav", Null, Array(":http-caching=5")
          'vlc.playlist.Add "http://192.168.1.40:8080/audio.wav", Null, Null
          vlc.playlist.playItem (0)
          
          MsgBox "Hello world!"
          
          vlc.playlist.stop
      End Sub
      


      ":network-caching=5" совместно с ":http-caching=5" даёт тот же результат, что и один ":network-caching=5".
      Null и ":http-caching=5" тоже дают одинаковые результаты.
      К сожалению, эффекта от ":http-caching=5" нет.
  • +1
    >> Я мог бы воспользоваться и основной камерой

    Хм. Призма? Два зеркала, на худой конец :)
    • 0
      Вполне возможный вариант, заодно и вопрос противовеса будет решён. Правда выглядить будет тяжеловато.
  • –2
    Какой то серьезный лаг, у меня почти такой, a же а железка в разы слабее. www.youtube.com/watch?v=hixjlr1w5XY
  • +1
    Девушка на видео симпатичная какая… :)
    • +1
      Это всё опять-таки Митя и его личная жизнь. Ну вот как бы я сказал, «Маша, давай я сниму видеоролик с тобой и выложу в YouTube...»? Наверняка не прокатит. А ему всё сходит с рук! Ну или что там у него…
      • 0
        Мне кажется, что Машу робот Митя интересует довольно опосредованно, что бы обратить на себя внимание вовсе не его. :)

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