インテル® INDE Media for Mobile チュートリアル – Android* 上での Qt* Quick アプリケーションのビデオ・キャプチャー
この記事は、インテル® デベロッパー・ゾーンに掲載されている「Intel® INDE Media for Mobile Tutorials – Video Capturing for Qt* Quick Applications on Android*」の日本語参考訳です。
このチュートリアルは、インテル® INDE Media for Mobile を使用して、Android* 上の 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] をクリックします。その後、マニフェストの機能と権限を調整します。
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/ フォルダーに保存されます。