インテル® INDE Media for Mobile チュートリアル – Android* 上での Qt* Quick アプリケーションのビデオ・キャプチャー

同カテゴリーの次の記事

成功した開発者の 5 つの秘訣

この記事は、インテル® デベロッパー・ゾーンに掲載されている「Intel® INDE Media for Mobile Tutorials – Video Capturing for Qt* Quick Applications on Android*」の日本語参考訳です。


このチュートリアルは、インテル® INDE Media for Mobile を使用して、Android* 上の Qt* Quick アプリケーションにビデオ・キャプチャーを追加する方法を説明します。

Qt* Quick キャプチャー

必要条件:

このチュートリアルは、経験豊富な Qt* プログラマー向けです。Android* 向け Qt* アプリケーションを始めて開発される方は、「Getting Started with Qt for Android」(英語) を確認してください。このチュートリアルでは、重要なポイントのみ説明します。

単純な QML アプリケーションを作成します。動くコンテンツ、FPS カウンター、録画ボタンが必要です。Qt* Quick 2 アプリケーション・プロジェクトを新規作成します。Qt* Quick 2.3 コンポーネント・セットを選択します。Qt* Creator により main.qml ファイルが自動生成されます。

import QtQuick 2.3
import QtQuick.Window 2.2

Window {
    ...
}

Window オブジェクトは、Qt* Quick シーンのトップレベル・ウィンドウを新規作成します。ここでは、その動作を変更する必要がありますが、Window のソースは変更できないため、QQuickWindow から継承します。

#ifndef QTCAPTURINGWINDOW_H
#define QTCAPTURINGWINDOW_H

#include <QQuickWindow>
#include <jni.h>

class QOpenGLFramebufferObject;
class QOpenGLShaderProgram;
class QElapsedTimer;

class QtCapturingWindow : public QQuickWindow
{
    Q_OBJECT
    Q_PROPERTY(bool isRunning READ isRunning NOTIFY isRunningChanged)
    Q_PROPERTY(int fps READ fps NOTIFY fpsChanged)

public:
    explicit QtCapturingWindow(QWindow *parent = 0);
    ~QtCapturingWindow();

    bool isRunning() const { return m_isRunning; }
    int fps() const { return m_fps; }

    Q_INVOKABLE void startCapturing(int width, int height, int frameRate, 
        int bitRate, QString fileName);
    Q_INVOKABLE void stopCapturing();

signals:
    void isRunningChanged();
    void fpsChanged();

private:
    jobject m_qtCapturingObject;
    QOpenGLFramebufferObject *m_fbo;
    QOpenGLShaderProgram *m_program;
    QElapsedTimer *m_timer;
    bool m_isRunning;
    int m_fps;

    QString m_videoDir;
    int m_videoFrameRate;

    void drawQuad(int textureID);
    void captureFrame(int textureID);

private slots:
    void onSceneGraphInitialized();
    void onBeforeRendering();
    void onAfterRendering();
};

#endif // QTCAPTURINGWINDOW_H

ビデオ・キャプチャー用の FPS カウンター Q_PROPERTY と開始/終了メソッド Q_INVOKABLE があることが分かります。

main.cpp ファイルを編集します。

>#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include "qtcapturingwindow.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    qmlRegisterType<QtCapturingWindow>("INDE.MediaForMobile", 1, 0, 
        "CapturingWindow");

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

これで、QML 内で FPS プロパティーと startCapturing/stopCapturing メソッドを使用できるようになります。

import QtQuick 2.3
import INDE.MediaForMobile 1.0

CapturingWindow {
    id: capturing
    visible: true
    color: "white"

    Rectangle {
        radius: 50
        width: (parent.width > parent.height ? 
            parent.width : parent.height) / 3
        height: width
        anchors.centerIn: parent
        gradient: Gradient {
            GradientStop { position: 0.0; color: "red" }
            GradientStop { position: 0.5; color: "yellow" }
            GradientStop { position: 1.0; color: "green" }
        }
        PropertyAnimation on rotation {
            running: Qt.application.state === Qt.ApplicationActive
            loops: Animation.Infinite
            easing.type: Easing.Linear
            from: 0
            to: 360
            duration: 8000
        }
    }

    Connections {
        target: Qt.application
        onStateChanged: {
            switch (Qt.application.state) {
            case Qt.ApplicationSuspended:
            case Qt.ApplicationHidden:
                if (capturing.isRunning)
                    capturing.stopCapturing();
                break
            }
        }
    }

    Rectangle {
        id: buttonRect
        width: 150
        height: 150
        radius: 75
        anchors {
            right: parent.right
            bottom: parent.bottom
            margins: 50
        }
        color: "green"
        Behavior on color {
            ColorAnimation { duration: 150 }
        }
        MouseArea {
            id: mouseArea
            anchors.fill: parent
            onClicked: {
                if (!capturing.isRunning) {
                    capturing.startCapturing(1280, 720, 30, 3000, 
                        "QtCapturing.mp4");
                } else {
                    capturing.stopCapturing();
                }
            }
        }
        states: [
            State {
                when: capturing.isRunning
                PropertyChanges {
                    target: buttonRect
                    color: "red"
                }
            }
        ]
    }

    Text {
        anchors {
            left: parent.left
            top: parent.top
            margins: 25
        }
        font.pixelSize: 50
        text: "FPS: " + capturing.fps
    }
}

https://www.izzz.us/article/intel-software-dev-products/intel-inde/ からインテル® INDE をダウンロードし、インストールします。インテル® INDE のインストール後に、Media for Mobile のダウンロードを選択し、インストールします。不明な点がある場合は、インテル® INDE のフォーラム (英語) を参照してください。<Media for Mobile のインストール・フォルダー>/libs にある 2 つの jar ファイル (android-<version>.jar と domain-<version>.jar) を /android-sources/libs/ フォルダーにコピーします。このフォルダーは、コピーする前に作成しておいてください。/android-sources/ は任意の名前にできますが、プロジェクト・ファイルで同じに設定する必要があります。

ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android-sources

Android* 上へのアプリケーションの配布 – 詳細は、このドキュメントを確認してください。

次に Java* コードを見てみましょう。メイン・アクティビティーのソースコードを変更するのは面倒です。代わりに、JNI を使用して、C++ で別のクラスを作成し、アプリケーションの起動時にインスタンス化したほうが簡単です。/android-sources/src/org/qtproject/qt5/android/bindings/ フォルダーを作成し、次のコードを含む Java* ファイル QtCapturing.java を追加します。

package org.qtproject.qt5.android.bindings;

import com.intel.inde.mp.IProgressListener;
import com.intel.inde.mp.domain.Resolution;
import com.intel.inde.mp.android.graphics.FullFrameTexture;

import android.os.Environment;
import android.content.Context;

import java.io.IOException;
import java.io.File;

public class QtCapturing
{
    private static FullFrameTexture texture;
    private VideoCapture videoCapture;

    private IProgressListener progressListener = new IProgressListener() {
        @Override
        public void onMediaStart() {
        }

        @Override
        public void onMediaProgress(float progress) {
        }

        @Override
        public void onMediaDone() {
        }

        @Override
        public void onMediaPause() {
        }

        @Override
        public void onMediaStop() {
        }

        @Override
        public void onError(Exception exception) {
        }
    };

    public QtCapturing(Context context)
    {
        videoCapture = new VideoCapture(context, progressListener);

        texture = new FullFrameTexture();
    }

    public static String getDirectoryDCIM()
    {
        return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + File.separator;
    }

    public void initCapturing(int width, int height, int frameRate, 
        int bitRate)
    {
        VideoCapture.init(width, height, frameRate, bitRate);
    }

    public void startCapturing(String videoPath)
    {
        if (videoCapture == null) {
            return;
        }
        synchronized (videoCapture) {
            try {
                videoCapture.start(videoPath);
            } catch (IOException e) {
            }
        }
    }

    public void captureFrame(int textureID)
    {
        if (videoCapture == null) {
            return;
        }
        synchronized (videoCapture) {
            videoCapture.beginCaptureFrame();
            texture.draw(textureID);
            videoCapture.endCaptureFrame();
        }
    }

    public void stopCapturing()
    {
        if (videoCapture == null) {
            return;
        }
        synchronized (videoCapture) {
            if (videoCapture.isStarted()) {
                videoCapture.stop();
            }
        }
    }

}

同じフォルダーに、次のコードを含む VideoCapture.java という名前の別の Java* ファイルを作成します

package org.qtproject.qt5.android.bindings;

import android.content.Context;
import com.intel.inde.mp.*;
import com.intel.inde.mp.android.AndroidMediaObjectFactory;
import com.intel.inde.mp.android.AudioFormatAndroid;
import com.intel.inde.mp.android.VideoFormatAndroid;

import java.io.IOException;

public class VideoCapture
{
    private static final String TAG = "VideoCapture";

    private static final String Codec = "video/avc";
    private static int IFrameInterval = 1;

    private static final Object syncObject = new Object();
    private static volatile VideoCapture videoCapture;

    private static VideoFormat videoFormat;
    private static int videoWidth;
    private static int videoHeight;
    private GLCapture capturer;

    private boolean isConfigured;
    private boolean isStarted;
    private long framesCaptured;
        private Context context;
        private IProgressListener progressListener;

    public VideoCapture(Context context, IProgressListener progressListener)
    {
        this.context = context;
        this.progressListener = progressListener;
    }

    public static void init(int width, int height, int frameRate, 
        int bitRate)
    {
        videoWidth = width;
        videoHeight = height;

        videoFormat = new VideoFormatAndroid(Codec, videoWidth, videoHeight);
        videoFormat.setVideoFrameRate(frameRate);
        videoFormat.setVideoBitRateInKBytes(bitRate);
        videoFormat.setVideoIFrameInterval(IFrameInterval);
    }

    public void start(String videoPath) throws IOException
    {
        if (isStarted())
            throw new IllegalStateException(TAG + " already started!");

        capturer = 
            new GLCapture(new AndroidMediaObjectFactory(context), 
                progressListener);
        capturer.setTargetFile(videoPath);
        capturer.setTargetVideoFormat(videoFormat);

        AudioFormat audioFormat = 
            new AudioFormatAndroid("audio/mp4a-latm", 44100, 2);
        capturer.setTargetAudioFormat(audioFormat);

        capturer.start();

        isStarted = true;
        isConfigured = false;
        framesCaptured = 0;
    }

    public void stop()
    {
        if (!isStarted())
            throw new IllegalStateException(TAG + 
                " not started or already stopped!");

        try {
            capturer.stop();
            isStarted = false;
        } catch (Exception ex) {
        }

        capturer = null;
        isConfigured = false;
    }

    private void configure()
    {
        if (isConfigured())
            return;

        try {
            capturer.setSurfaceSize(videoWidth, videoHeight);
            isConfigured = true;
        } catch (Exception ex) {
        }
    }

    public void beginCaptureFrame()
    {
        if (!isStarted())
            return;

        configure();
        if (!isConfigured())
                return;

        capturer.beginCaptureFrame();
    }

    public void endCaptureFrame()
    {
        if (!isStarted() || !isConfigured())
            return;

        capturer.endCaptureFrame();
        framesCaptured++;
    }

    public boolean isStarted()
    {
        return isStarted;
    }

    public boolean isConfigured()
    {
        return isConfigured;
    }

}

次に、ほかの Android* アプリケーションと同様にマニフェスト XML ファイルを設定する必要があります。マニフェスト・ファイルは、起動するアクティビティーとアクセス可能な関数をコンパイル時に指定します。[Projects] タブに移動し、Android* キットを Run 設定に切り替えてから、[Deploy configurations] を展開して、[Create AndroidManifest.xml] ボタンをクリックします。ウィザードで [Finish] をクリックします。その後、マニフェストの機能と権限を調整します。

AndroidManifest.xml

XML Source に切り替えます。アクティビティー・テーマを設定して、横長の向きにロックし、ステータスバーを非表示にします。

<activity
    ...
    <activity ... android:screenOrientation="landscape" 
        android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
    ...
</activity>

すべての必要なファイルをプロジェクト・ファイルに追加します。

OTHER_FILES += \
    android-sources/libs/android-1.2.2415.jar \
    android-sources/libs/domain-1.2.2415.jar \
    android-sources/src/org/qtproject/qt5/android/bindings/QtCapturing.java \
    android-sources/src/org/qtproject/qt5/android/bindings/VideoCapture.java \
    android-sources/AndroidManifest.xml

次のようなプロジェクト構造になります。

プロジェクト構造

基本機能は qtcapturingwindow.cpp ファイルにまとまっています。まず、Java* コードと関連付ける必要があります。JNI_OnLoad でクラス・オブジェクト参照を探し取得します。

#include "qtcapturingwindow.h"
#include <QDebug>
#include <QOpenGLFramebufferObject>
#include <QOpenGLShaderProgram>
#include <QElapsedTimer>
#include <QtAndroid>

static JavaVM *s_javaVM = 0;
static jclass s_classID = 0;

static jmethodID s_constructorMethodID = 0;
static jmethodID s_initCapturingMethodID = 0;
static jmethodID s_startCapturingMethodID = 0;
static jmethodID s_captureFrameMethodID = 0;
static jmethodID s_stopCapturingMethodID = 0;

static jmethodID s_getDirectoryDCIMMethodID =0;

// このメソッドはモジュールがロードされた直後に呼び出されます
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void */*reserved*/)
{
    JNIEnv *env;
    if (vm->GetEnv(reinterpret_cast<void **>(&env), 
        JNI_VERSION_1_6) != JNI_OK) {
            qCritical() << "Can't get the enviroument";
            return -1;
    }

    s_javaVM = vm;
    // クラスを探します
    jclass clazz = 
        env->FindClass("org/qtproject/qt5/android/bindings/QtCapturing");
    if (!clazz) {
        qCritical() << "Can't find QtCapturing class";
        return -1;
    }
    // クラスへのグローバル参照を保持します
    s_classID = (jclass)env->NewGlobalRef(clazz);

    // クラスのコンストラクターを探します
    s_constructorMethodID = env->GetMethodID(s_classID, "<init>", 
        "(Landroid/content/Context;)V");
    if (!s_constructorMethodID) {
        qCritical() << "Can't find QtCapturing class contructor";
        return -1;
    }

    s_initCapturingMethodID = 
        env->GetMethodID(s_classID, "initCapturing", "(IIII)V");
    if (!s_initCapturingMethodID) {
        qCritical() << "Can't find initCapturing() method";
        return -1;
    }

    s_startCapturingMethodID = 
        env->GetMethodID(s_classID, "startCapturing", 
            "(Ljava/lang/String;)V");
    if (!s_startCapturingMethodID) {
        qCritical() << "Can't find startCapturing() method";
        return -1;
    }

    s_captureFrameMethodID = 
        env->GetMethodID(s_classID, "captureFrame", "(I)V");
    if (!s_startCapturingMethodID) {
        qCritical() << "Can't find captureFrame() method";
        return -1;
    }

    s_stopCapturingMethodID = 
        env->GetMethodID(s_classID, "stopCapturing", "()V");
    if (!s_stopCapturingMethodID) {
        qCritical() << "Can't find stopCapturing() method";
        return -1;
    }

    // 静的メソッドを登録し呼び出します
    s_getDirectoryDCIMMethodID = env->GetStaticMethodID(s_classID, 
        "getDirectoryDCIM", "()Ljava/lang/String;");
    if (!s_getDirectoryDCIMMethodID) {
        qCritical() << "Can't find getDirectoryDCIM() static method";
        return -1;
    }

    return JNI_VERSION_1_6;
}

このウィンドウに新しい OpenGL* コンテキストが作成されると、QQuickWindow::sceneGraphInitialized() シグナルが出力されます。QQuickWindow::beforeRendering() シグナルは、スクリーンがレンダリングを開始する前に出力され、QQuickWindow::afterRendering() シグナルは、スクリーンのレンダリングが終了し、swapbuffers が呼び出される前に出力されます。これらのシグナルが通知されるように Qt::DirectConnection を設定します。

QtCapturingWindow::QtCapturingWindow(QWindow *parent)
    : QQuickWindow(parent)
    , m_qtCapturingObject(nullptr)
    , m_fbo(nullptr)
    , m_program(nullptr)
    , m_isRunning(false)
    , m_fps(0)
{
    connect(this, &QtCapturingWindow::sceneGraphInitialized, this, 
        &QtCapturingWindow::onSceneGraphInitialized, Qt::DirectConnection);
    connect(this, &QtCapturingWindow::beforeRendering, this, 
        &QtCapturingWindow::onBeforeRendering, Qt::DirectConnection);
    connect(this, &QtCapturingWindow::afterRendering, this, 
        &QtCapturingWindow::onAfterRendering, Qt::DirectConnection);

    m_timer = new QElapsedTimer();
    m_timer->start();
}

これで、OpenGL* コンテキストを取得し、QtCapturing.java クラスをインスタンス化できます。QOpenGLFramebufferObject クラスは、OpenGL* フレームバッファー・オブジェクトをカプセル化します。フレームバッファーに奥行きをアタッチするのを忘れないでください。

void QtCapturingWindow::onSceneGraphInitialized()
{
    // QtCapturing の新しいインスタンスを作成します
    JNIEnv *env;
    // Qt* は Java* スレッドとは別のスレッドで実行しているため、
    // 必ず Java* VM を現在のスレッドにアタッチする必要があります
    if (s_javaVM->AttachCurrentThread(&env, NULL) < 0) {
        qCritical( ) << "AttachCurrentThread failed";
        return;
    }

    m_qtCapturingObject = 
        env->NewGlobalRef(env->NewObject(s_classID, s_constructorMethodID, 
        QtAndroid::androidActivity().object<jobject>()));
    if (!m_qtCapturingObject) {
        qCritical() << "Can't create the QtCapturing object";
        return;
    }

    // DCIM ディレクトリーを取得します
    jstring value = (jstring)env->CallStaticObjectMethod(s_classID, 
        s_getDirectoryDCIMMethodID);
    const char *res = env->GetStringUTFChars(value, NULL);
    m_videoDir = QString(res);
    env->ReleaseStringUTFChars(value, res);

    // 現在のスレッドからデタッチするのを忘れないでください
    s_javaVM->DetachCurrentThread();

    m_fbo = new QOpenGLFramebufferObject(size());
    m_fbo->setAttachment(QOpenGLFramebufferObject::Depth);
}

Qt* Android* Extras モジュールにリンクするには、この文をプロジェクト・ファイルに追加します。

QT += androidextras

すべてのリソースを適切な方法で解放するのを忘れないでください。

QtCapturingWindow::~QtCapturingWindow()
{
    if (m_isRunning)
        stopCapturing();

    delete m_fbo;
    delete m_timer;
}

QQuickWindow::setRenderTarget() メソッドは、このウィンドウのレンダリング・ターゲットを設定します。デフォルト値は 0 で、ウィンドウのサーフェスにレンダリングします。

void QtCapturingWindow::onBeforeRendering()
{
    if (m_isRunning) {
        if (renderTarget() == 0) {
            setRenderTarget(m_fbo);
        }
    } else {
        if (renderTarget() != 0) {
            setRenderTarget(0);
        }
    }
}

これで、QML シーンがレンダリングされたテクスチャーが得られます。次に、このテクスチャーをビデオサーフェスへレンダリングして表示する必要があります。

void QtCapturingWindow::onAfterRendering()
{
    static qint64 frameCount = 0;
    static qint64 fpsUpdate = 0;
    static const int fpsUpdateRate = 4; // 1 秒あたりの更新回数
    static qint64 m_nextCapture = 0;

    if (m_isRunning) {
        // フルスクリーンに 4 つ描画します
        QOpenGLFramebufferObject::bindDefault();
        drawQuad(m_fbo->texture());

        // 実際にキャプチャーするため Java* にカラー・アタッチメントを渡します
        if (m_timer->elapsed() > m_nextCapture) {
            captureFrame(m_fbo->texture());
            m_nextCapture += 1000 / m_videoFrameRate;
        }
    }

    // fps を更新します
    frameCount++;
    if (m_timer->elapsed() > fpsUpdate) {
        fpsUpdate += 1000 / fpsUpdateRate;
        m_fps = frameCount * fpsUpdateRate;
        frameCount = 0;
        emit fpsChanged();
    }
}

このメソッドは、フルスクリーンのレンダリングを行います。

void QtCapturingWindow::drawQuad(int textureID)
{
    if (!m_program) {
        m_program = new QOpenGLShaderProgram();
        m_program->addShaderFromSourceCode(QOpenGLShader::Vertex,
                           "attribute highp vec4 vertices;"
                           "varying highp vec2 coords;"
                           "void main() {"
                           "    gl_Position = vertices;"
                           "    coords = (vertices.xy + 1.0) * 0.5;"
                           "}");
        m_program->addShaderFromSourceCode(QOpenGLShader::Fragment,
                           "uniform sampler2D texture;"
                           "varying highp vec2 coords;"
                           "void main() {"
                           "    gl_FragColor = texture2D(texture, coords);"
                           "}");

        m_program->bindAttributeLocation("vertices", 0);

        if (!m_program->link()) {
            qDebug() << "Link wasn't successful: " << m_program->log();
        }
    }

    m_program->bind();
    m_program->enableAttributeArray(0);

    float values[] = {
        -1, -1,
        1, -1,
        -1,  1,
        1,  1
    };

    m_program->setAttributeArray(0, GL_FLOAT, values, 2);
    glBindTexture(GL_TEXTURE_2D, textureID);
    glViewport(0, 0, size().width(), size().height());
    glDisable(GL_DEPTH_TEST);
    glClearColor(0, 0, 0, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    m_program->disableAttributeArray(0);
    m_program->release();
}

キャプチャーを開始するたびに出力ビデオ・フォーマットを設定することで、呼び出しと呼び出しの間にパラメーターを変更できるようにしています。

void QtCapturingWindow::startCapturing(int width, int height, int frameRate, int bitRate, QString videoName)
{
    if (!m_qtCapturingObject)
        return;

    m_isRunning = true;
    emit isRunningChanged();

    JNIEnv *env;
    if (s_javaVM->AttachCurrentThread(&env, NULL) < 0) {
        qCritical() << "AttachCurrentThread failed";
        return;
    }

    // フォーマットのセットアップを行います
    m_videoFrameRate = frameRate;
    env->CallVoidMethod(m_qtCapturingObject, s_initCapturingMethodID, 
        width, height, frameRate, bitRate);

    // キャプチャーを開始します
    QString videoPath = m_videoDir + videoName;
    jstring string = 
        env->NewString(reinterpret_cast<const jchar *>(
        videoPath.constData()), videoPath.length());
    env->CallVoidMethod(m_qtCapturingObject, s_startCapturingMethodID, 
        string);
    env->DeleteLocalRef(string);

    s_javaVM->DetachCurrentThread();
}

次のように、テクスチャー・ハンドルを QtCapturing.java クラスの captureFrame() メソッドに渡します。

void QtCapturingWindow::captureFrame(int textureID)
{
    if (!m_qtCapturingObject)
        return;

    JNIEnv *env;
    if (s_javaVM->AttachCurrentThread(&env, NULL) < 0) {
        qCritical() << "AttachCurrentThread failed";
        return;
    }

    env->CallVoidMethod(m_qtCapturingObject, s_captureFrameMethodID, 
        textureID);

    s_javaVM->DetachCurrentThread();
}

最後にキャプチャーを終了します。

void QtCapturingWindow::stopCapturing()
{
    if (!m_qtCapturingObject)
        return;

    m_isRunning = false;
    emit isRunningChanged();

    JNIEnv *env;
    if (s_javaVM->AttachCurrentThread(&env, NULL) < 0) {
        qCritical() << "AttachCurrentThread failed";
        return;
    }

    env->CallVoidMethod(m_qtCapturingObject, s_stopCapturingMethodID);

    s_javaVM->DetachCurrentThread();
}

Qt* Quick アプリケーションにビデオ・キャプチャー機能を追加するのに必要な作業はこれだけです。テスト・アプリケーションを実行してみてください。キャプチャーしたビデオは、Android* デバイスの /mnt/sdcard/DCIM/ フォルダーに保存されます。

関連記事