Время прочтения: 7 мин.

Обработки каждого приходящего кадра производятся посредством библиотеки FFmpeg, предоставляющей широкие возможности аппаратного кодирования и декодирования аудио и видео потоков. Целью написания данного модуля является его последующее использование в приложении построения виртуальной реальности.

В последние годы робототехнические комплексы используются в широком перечне различных областей человеческой деятельности. Наиболее важными направлениями внедрения роботов являются те, в которых непосредственное участие человека нежелательно в связи с угрозой его здоровью или даже жизни из-за воздействующих опасных факторов.

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

Существенный скачок в качестве и доступности средств виртуальной реальности позволяет говорить о перспективности использования данного направления для отображения графической и видеоинформации оператору РТК. Управление роботом может осуществляться качественнее при правильном восприятии окружающей обстановки без задержек и помех.

Процесс состоит из нескольких этапов:

1. Постановка задачи

Разработать ПО, осуществляющее трансляцию потокового видео с IP-камеры в режиме реального времени с минимальными задержками. Поскольку для построения виртуальной реальности была выбрана среда Unity 3D, результирующее видео должно отображаться в ней посредством стандартных объектов. Также должно быть предусмотрено необходимое масштабирование полученного изображения при изменении размеров объекта, играющего для него роль экрана.

2. Анализ задачи

При изучении способа трансляции потокового видео с IP-камеры в среде Unity3D по протоколу HTTP с использованием классов из пространства имен System.Net, была написана простая программа, отображающая видео на стандартный 3D-объект Unity Cube. При этом было замечено, что вывод изображения происходит со значительной задержкой, поэтому было принято решение о ее минимизации, посредством передачи массива видеоданных по протоколу RTP. Для обработки приходящего кадра в этом случае целесообразно использовать библиотеку FFmpeg, предоставляющую широкие возможности аппаратного кодирования и декодирования аудио и видео потоков. Использование аппаратных ресурсов компьютера поможет в данном случае существенно снизить нагрузку на центральный процессор, что повысит общую скорость работы приложения.

Основной последовательностью действий FFmpeg при декодировании видеопотока являются: регистрация в системе всех имеющихся кодеков, поиск подходящего кодека, выделение памяти для контекста кодека, выделение памяти для кадра приема видео, предоставление декодеру на вход «сырого» набора байт потока и получение на выходе готового кадра. Для корректной работы программы необходимо наличие подключенной к одной локальной сети с компьютеров IP-камеры. В настройках камеры требуется задать Basic-аутентификацию, а также передачу видеопотока в формате H264.

3. Реализация

Так как библиотека FFmpeg написана на языке C/C++ (неуправляемый код), для использования ее возможностей в Unity3D (C#) (управляемый код) необходимо с помощью технологии PInvoke (Platform Invoke) создать для нее обертку. Поиск существующих полностью рабочих решений данной задачи не дал результата. Однако при написании собственной обертки за основу был взят код проекта под названием SharpFFmpeg. В итоге был реализован импорт функций из файлов avcodec-57.dll, avformat-57.dll, avutil-55.dll, swscale-4.dll.

Для непосредственной работы с библиотекой FFmpeg через созданную ранее обертку создан класс FFmpegWorker. Вначале осуществляется регистрация всех кодеков, анализаторов, битовых фильтров для libavcodec, а также глобальная инициализация сетевых компонентов посредством функций av_register_all и avformat_network_init.

Затем функциями avformat_open_input и avformat_find_stream_info производится разбор заголовка входящего видеопотока для определения кодека, используемого при его кодировании. Далее с помощью функций avcodec_find_decoder и avcodec_open2 ищется соответствующий ему декодер и инициализируется контекст кодека.

Для декодирования каждого кадра видеопотока используются функции av_read_frame и avcodec_decode_video2. При необходимости выполняется масштабирование полученного изображения или его части посредством функции sws_scale. Ниже приведен код реализации класса FFmpegWorker:

class FFmpegWorker
{
    IntPtr CodecContextPtr, SwsContextPtr, FormatContextPtr, FramePtr, PicturePtr, PacketPtr;
    int videoStream, width, height;
    string rtspURL;
    bool stop;

    // Событие - отправить кадр декодироваться
    public delegate void ShowTextureOnCubeHandler(MemoryStream ms);

    public event ShowTextureOnCubeHandler ShowTextureOnCube;

    void EmitShowTextureOnCube(MemoryStream ms)
    {
        if (ShowTextureOnCube != null)
            ShowTextureOnCube.Invoke(ms);
    }

    public FFmpegWorker()
    {
        videoStream = -1;
    }

    ~FFmpegWorker() // Освобождение занимаемых ресурсов
    {
        // Закрыть соединение с видеофайлом
        FFmpeg.avformat_close_input(out FormatContextPtr);
        // Закрыть кодек
        FFmpeg.avcodec_close(CodecContextPtr);
    }

    public int Initial(string url)
    {
        // Определение кодека, инициализация его контекста
        stop = false;
        int err;
        rtspURL = url;
        FFmpeg.av_register_all(); // Инициализация библиотеки libavformat и регистрация всех обработчиков и протоколов
        FFmpeg.avformat_network_init(); // Глобальная инициализация сетевых компонентов
        FormatContextPtr = FFmpeg.avformat_alloc_context(); // выделение памяти для AVFormatContext.
        FramePtr = FFmpeg.av_frame_alloc(); // Выделение памяти для AVFrame и установка значений по умолчанию
        err = FFmpeg.avformat_open_input(out FormatContextPtr, rtspURL, null, null); // Открытие входящего потока и чтение заголовка
        if (err < 0)
        {
            HelpFunctions.AddMessageToLog(Encoding.UTF8.GetBytes("Не удалось открыть поток!"));
            return -1;
        }
        if (FFmpeg.avformat_find_stream_info(FormatContextPtr, null) < 0) // Чтение пакетов медиафайла для получения информации о потоке
        {
            HelpFunctions.AddMessageToLog(Encoding.UTF8.GetBytes("Не удалось получить информацию о потоке!"));
            return -1;
        }
        videoStream = -1;
        FFmpeg.AVFormatContext formatContext = (FFmpeg.AVFormatContext)Marshal.PtrToStructure(FormatContextPtr, typeof(FFmpeg.AVFormatContext));
        uint StreamsCount = formatContext.nb_streams;
        IntPtr[] AVStreamArray = new IntPtr[StreamsCount];
        Marshal.Copy(formatContext.streams, AVStreamArray, 0, (int)StreamsCount);
        FFmpeg.AVStream VideoStream;
        FFmpeg.AVCodecContext ContextVideoCodec = new FFmpeg.AVCodecContext();
        for (int i = 0; i < StreamsCount; i++)
        {
            VideoStream = (FFmpeg.AVStream)Marshal.PtrToStructure (AVStreamArray[i], typeof(FFmpeg.AVStream));
            CodecContextPtr = VideoStream.codec;
            ContextVideoCodec = (FFmpeg.AVCodecContext)Marshal. PtrToStructure(CodecContextPtr, typeof(FFmpeg.AVCodecContext));
            if (ContextVideoCodec.codec_type == FFmpeg.AVMediaType.AVMEDIA_TYPE_VIDEO)
            {
                videoStream = i;
                break;
            }
        }
        if (videoStream == -1)
        {
            HelpFunctions.AddMessageToLog(Encoding.UTF8.GetBytes("Не удалось найти видеопоток!"));
            return -1;
        }
        width = ContextVideoCodec.width;
        height = ContextVideoCodec.height;
        FFmpeg.avpicture_alloc(out PicturePtr, FFmpeg.AVPixelFormat. AV_PIX_FMT_RGB24, ContextVideoCodec.width, ContextVideoCodec.height);
        IntPtr CodecPtr = FFmpeg.avcodec_find_decoder(ContextVideoCodec. codec_id); // Найдите зарегистрированный декодер с соответствующим идентификатором кодека
        if (CodecPtr.Equals(default(IntPtr)))
        {
            HelpFunctions.AddMessageToLog(Encoding.UTF8.GetBytes("Кодек не найден!"));
            return -1;
        }
        SwsContextPtr = FFmpeg.sws_getContext(width, height, FFmpeg.AVPixelFormat.AV_PIX_FMT_YUV420P, width, height, FFmpeg.AVPixelFormat.AV_PIX_FMT_RGB24, FFmpeg.SWS_BICUBIC, null, null, null); // Выделить и вернуть SwsContext
        HelpFunctions.AddMessageToLog(Encoding.UTF8.GetBytes(width + " x " + height));
        if (FFmpeg.avcodec_open2(CodecContextPtr, CodecPtr, null) < 0) 
// Инициализировать AVCodecContext для использования данного AVCodec
        {
            HelpFunctions.AddMessageToLog(Encoding.UTF8.GetBytes("Не удалось открыть кодек!"));
            return -1;
        }
        HelpFunctions.AddMessageToLog(Encoding.UTF8.GetBytes("Успешная инициализация!"));
        return 0;
    }

    public int H264Decodec()
    {
        IntPtr frameFinished = new IntPtr(0);
        FFmpeg.AVPacket packet;
        FFmpeg.AVFrame decodeFrame;
        FFmpeg.AVPicture Picture = (FFmpeg.AVPicture)Marshal. PtrToStructure(PicturePtr, typeof(FFmpeg.AVPicture));
        while (FFmpeg.av_read_frame(FormatContextPtr, out PacketPtr) >= 0)
        {
            packet = (FFmpeg.AVPacket)Marshal.PtrToStructure(PacketPtr, typeof(FFmpeg.AVPacket));
            if (packet.stream_index == videoStream)
            {
                FFmpeg.avcodec_decode_video2(CodecContextPtr, FramePtr, out frameFinished, PacketPtr);
                if (frameFinished.ToInt32() != 0)
                {
                    decodeFrame = (FFmpeg.AVFrame)Marshal.PtrToStructure (FramePtr, typeof(FFmpeg.AVFrame));
                    int rs = FFmpeg.sws_scale(SwsContextPtr, decodeFrame.data, decodeFrame.linesize, 0, height, out Picture.data, out Picture.linesize);
                    if (rs != -1)
                    {
                        int lineSize = Picture.linesize[0];
                        byte[] JpegData = new byte[lineSize];
                        Marshal.Copy(Picture.data[0], JpegData, 0, lineSize);
                        MemoryStream ms = new MemoryStream(JpegData, 0, lineSize, false, true);
                        EmitShowTextureOnCube(ms);
                    }
                }
            }
        }
        return 1;
    }
}

В основной программе – скрипте, в отдельном потоке, осуществляется работа класса FFmpegWorker. При получении декодированного изображения в классе FFmpegWorker вызывается событие основного скрипта, загружающее это изображение в текстуру 3D-объекта Cube, играющего роль экрана для отображения результирующего видео. Код ниже демонстрирует содержимое этого скрипта:

public class MainRTSPClient : MonoBehaviour
{
    public GameObject IpCamCube;
    private Texture2D IpCameraTexture;
    private Queue<Task> TaskQueue;
    private object _queueLock;
    FFmpegWorker ffmpeg; // global data for ffmpeg event
    RTSPWorker rtspWorker;
    Thread rtspThread;

    public void Start()
    {
        IpCameraTexture = new Texture2D(1, 1, TextureFormat.RGB24, true);
        IpCamCube.GetComponent<Renderer>().material.mainTexture = IpCameraTexture;
        TaskQueue = new Queue<Task>();
        _queueLock = new object();
        ffmpeg = new FFmpegWorker();
        ffmpeg.ShowTextureOnCube += ShowPicture;
        rtspWorker = new RTSPWorker();
        rtspWorker.setRtspURL("D:\\Augmented_Reality\\HDV_1130.MP4");
        rtspWorker.setFFmpeg(ffmpeg);
        rtspThread = new Thread(rtspWorker.Run)
        {
            IsBackground = true
        };
        rtspThread.Start();
    }

    private void Update()
    {
        lock (_queueLock)
        {
            if (TaskQueue.Count > 0)
                TaskQueue.Dequeue()();
        }
    }

    public void Execute(Task newTask)
    {
        lock (_queueLock)
        {
            TaskQueue.Enqueue(newTask);
        }
        try
        {
            Thread.Sleep(100);
        }
        catch (ThreadInterruptedException) { }
    }

    void ShowPicture(MemoryStream ms)
    {
        Execute(() =>
        {
            IpCameraTexture.LoadImage(ms.GetBuffer());
        });
    }
}

В ходе проделанных работ была разработана программа-скрипт, осуществляющая трансляцию потокового видео с IP-камеры в режиме реального времени по протоколу RTP. Для декодирования приходящих кадров используется библиотека FFmpeg, позволяющая использовать возможности аппаратной платформы компьютера. Для подключения этой библиотеки в среду Unity 3D с помощью технологии PInvoke была создана специальная обертка. Результирующее видео отображается на стандартном объекте Unity — Cube с учетом необходимого масштабирования изображения. В дальнейшем данный модуль будет использован в приложении построения виртуальной реальности.