Уже сейчас Qt – неплохая среда для разработки мобильных приложений, однако некоторые моменты там остаются недоработанными. Так, например, если попробовать запустить стандартный пример с камерой, он будет работать в системе Windows, но не на Android. При этом примеры, использующие камеру в через Qml, вполне рабочие. А значит работа с камерой на Android — реализована, но полного доступа к ней нет. А если мы хотим свободы иметь доступ к видеопотоку?

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

Перед тем, как начать объяснение, стоит предупредить, что необязательно делать все описанное ниже, чтобы получить отдельные снимки. Вы можете просто использовать камеру через Qml и написать свой компонент на нем, чтобы захватывать отдельные кадры. А как, написано здесь.

Чтобы не писать все с нуля, возьмём тот самый пример Qt с названием “Camera Example” (который на скриншоте) и заставим его работать. Для вывода изображения в нем используется объект класса QCameraViewfinder. Мы напишем вместо него свой. И для вывода нам придется использовать OpenGL.

Для написания собственных классов вывода кадров, получаемых от медиа-объектов, в Qt заготовлен абстрактный класс QAbstractVideoSurface с виртуальными функциями, через которые происходит взаимодействие. Создадим свой класс на основе него, который будет отвечать за получение кадра, и назовем его CameraSurface. А за вывод кадра будет отвечать класс CameraSurfaceWidget наследуемый от QOpenGLWidget. Можно было бы и объединить эти два класса, но при наследовании от QAbstractVideoSurface и QOpenGLWidget возникает двойное наследование от класса QObject. А так делать нельзя.

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

Новый кадр мы получаем в функции bool CameraSurface::present(const QVideoFrame& frame). Параметр frame и есть тот самый новый кадр нашего видеопотока. Данные, которые могут прийти с камеры, могут быть в виде массива (так происходит в Windows или Symbian) или в виде текстуры (в Android). И если вам пришла текстура не вздумайте сразу ее считывать. При вызове frame.handle() вы можете подумать, что вы всего-то получаете индекс текстуры, но на самом деле в этот же момент происходит хитрая инициализация ресурсов на основе контекста OpenGL вашего потока. Но вызывается эта функция не в вашем потоке, а значит, этот контекст OpenGL здесь не будет работать. И пусть ключевое слово const в объявлении функции вас не обманывает, данные внутри коварно помечены как mutable. Просто копируйте кадр (frame) и читайте данные при рисовании.

Но это не все, что необходимо знать. При связывании с камерой, у нашего CameraSurface появляется скрытое свойство «GLContext», и ожидается, что вы запишите туда свой контекст OpenGL. И сделать это лучше в потоке объекта CameraSurface, то есть, используя вызов слота через функционал сигналов и слотов Qt. А потом отправьте событие, говорящее о записи в «GLContext», через объект свойства «_q_GLThreadCallback». И это событие должно иметь код QEvent::User. По идее это пользовательский тип события, но ведь вы вообще не должны были знать об этих костылях, так что плевать. Вообще, в Windows все работает и без действий, но если это не сделать на Android, то камера просто не начнет присылать кадры.

Короче, код рисования будет примерно такой:

void CameraSurfaceWidget::paintGL()
{
    if (!_surface->isActive()) {//если мы не начали принимать кадры с камеры, то
        _surface->scheduleOpenGLContextUpdate();//нужно отправить данные о контексте OpenGL
        QObject* glThreadCallback = (_surface->property("_q_GLThreadCallback")).value<QObject*>();//куда отправляем событие, говорящее, 
//что все готово к принятию видеопотока?
        if (glThreadCallback) {
            QEvent event(QEvent::User);//Событие с пользовательским флагом
            glThreadCallback->event(&event);//теперь его отправляем
        }
        //И эта часть выше не нужна для винды. Но, главное, она там ничего не сломает.
    } else {
        QVideoFrame& frame = _surface->frame();
       //рисование кадра
   }
}

В результате получаем возможность обрабатывать поток и виндоподобный интерфейс на Android. Данные из текстуры кадра, кстати, можно вытащить, используя Frame Buffer Object и glReadPixels (glGetTexImage в OpenGL ES нет). И это не единственный способ это сделать. Можно еще получать кадры через QVideoProbe, но тогда все видимо обрабатывается на процессоре, потому что дико лагает. Так что вообще-то лучше просто про это забудьте.

Еще странности Qt
И еще одна странная вещь напоследок. Если формат кадра — Format_RGB32, то каналы цвета располагаются в порядке B G R. Если формат — Format_BGR32, то R G B. Что-то в Qt перепутали.

Скачать исправленный пример можно здесь.

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


  1. ncix
    01.04.2015 17:54

    И еще одна странная вещь напоследок. Если формат кадра — Format_RGB32, то каналы цвета располагаются в порядке B G R. Если формат — Format_BGR32, то R G B

    QImage::Format_RGB32 4 The image is stored using a 32-bit RGB format (0xffRRGGBB)
    QVideoFrame::Format_BGR32 10 The frame is stored using a 32-bit BGR format (0xBBGGRRff)

    Имеете в виду, дока не соответствует коду? Может вы не в том порядке «вынимали» байты?


    1. MarkWatney Автор
      01.04.2015 19:42

      Да, похоже моя ошибка, не учел обратный порядок байт. Вот только напрямую с байтами я не работаю, данные приходят уже в виде текстуры, либо в виде массива и сразу отправляются в текстуру. А OpenGL, судя по всему, никак не учитывает этот порядок байт. Довольно не очевидная проблема по моему.