在 RTC 2020 編程挑戰賽春季賽中。咱們還有一個獲獎團隊,思路新穎,開發了一款基於雙人視頻聊天場景的小遊戲——「拿頭玩」。在視頻聊天過程當中便可開啓遊戲。經過人臉識別算法識別轉頭方向,實現以「接鍋」和「甩鍋」爲主題的玩法。目前實現了Android版本。java
如下爲「拿頭玩」團隊撰寫的開發思路與功能實現:git
《拿頭玩》是一款基於雙人視頻聊天場景的小遊戲,在視頻聊天過程當中便可開啓遊戲。經過人臉識別算法識別轉頭方向,實現以「接鍋」和「甩鍋」爲主題的玩法。目前實現了Android版本。github
頸椎問題是困擾全部辦公族的難題,大多數人工做中很難有機會能起身動一動,回到家裏也會由於疲倦而放棄作一些頸椎康復的運動。因此咱們想設計一款遊戲,讓你們在休息的時候能夠經過遊戲的形式活動頸椎,舒緩疼痛。咱們選擇了職場中的「甩鍋」和「接鍋」的場景,來做爲遊戲中的元素,但願能增長玩家的代入感。此外,咱們還添加了截圖分享模塊,方便遊戲進行傳播。算法
通過了5天的設計和開發,咱們最終完成了《拿頭玩》這個做品,下面來分享一下它的主要功能和其中的代碼細節。編程
視頻聊天模塊主要是使用聲網的音視頻sdk,它能夠快速的開發出一個基本的視頻對話模塊,核心代碼以下:app
//onCreate val rtcEngine = RtcEngine.create(this, AppConfig.appKey, object : IRtcEngineEventHandler() { override fun onFirstRemoteVideoDecoded(uid: Int,width: Int,height: Int,elapsed: Int) { setupRemoteVideo(uid) } } //setup private fun setupRemoteVideo(uid: Int) { val remoteView = RtcEngine.CreateRendererView(baseContext) remoteView.setZOrderMediaOverlay(true) container.addView(remoteView) rtcEngine.setupRemoteVideo(VideoCanvas(remoteView, VideoCanvas.RENDER_MODE_HIDDEN, uid)) }
爲了進行下一步的人臉識別,咱們須要獲取到視頻幀數據,對幀數據進行預處理。在閱讀聲網提供的文檔和demo後,咱們搭建了一個簡單的apm-plugin插件,經過這個插件,就能夠獲得視頻聊天過程當中的裸數據了。
首先咱們建立apm-plugin-packet-processing.cpp文件,而後經過CMakeLists.txt配置編譯參數:框架
cmake_minimum_required(VERSION 3.4.1) add_library( apm-plugin-packet-processing SHARED apm-plugin-packet-processing.cpp) include_directories(../cpp/include) //這裏須要導入sdk中的.h文件 ... target_link_libraries( apm-plugin-packet-processing ${log-lib})
而後咱們定義兩個jni方法來註冊和反註冊裸數據的回調:jvm
JNIEXPORT void JNICALL Java_com_zero_game_utils_frame_VideoFrameHandler_doRegisterProcessing (JNIEnv *env, jobject obj) { if (!rtcEngine) { return; } else { agora::util::AutoPtr<agora::media::IMediaEngine> mediaEngine; mediaEngine.queryInterface(rtcEngine, agora::AGORA_IID_MEDIA_ENGINE); s_packetObserver = *new AgoraVideoFrameObserver(jvm, env, env->NewGlobalRef(obj)); mediaEngine->registerVideoFrameObserver(&s_packetObserver); } } JNIEXPORT void JNICALL Java_com_zero_game_utils_frame_VideoFrameHandler_doUnregisterProcessing (JNIEnv *env, jobject obj) { if (!rtcEngine) { return; } else { agora::util::AutoPtr<agora::media::IMediaEngine> mediaEngine; mediaEngine.queryInterface(rtcEngine, agora::AGORA_IID_MEDIA_ENGINE); s_packetObserver.release(); mediaEngine->registerVideoFrameObserver(nullptr); } }
agora::media::IVideoFrameObserver這個接口就是聲網sdk提供的視頻幀回調,只要實現它便可:ide
class AgoraVideoFrameObserver : public agora::media::IVideoFrameObserver { public: AgoraVideoFrameObserver() { } AgoraVideoFrameObserver(JavaVM *vm, JNIEnv *env, jobject jobj) { //... } // 獲取本地攝像頭採集到的視頻幀 virtual bool onCaptureVideoFrame(VideoFrame &videoFrame) override { //processVideoFrame(videoFrame) return true; } // 獲取遠端用戶發送的視頻幀 virtual bool onRenderVideoFrame(unsigned int uid, VideoFrame &videoFrame) override { return true; } // 獲取本地視頻編碼前的視頻幀 virtual bool onPreEncodeVideoFrame(VideoFrame &videoFrame) override { return true; } void release() { //... } };
因爲Android平臺中攝像頭返回的裸數據是YUV420編碼,因此咱們還要轉換爲提供給人臉識別模塊的rgba格式才行,最後經過jni方法將數據傳遞到java層,進行後續的處理:post
int width = videoFrame.width; int height = videoFrame.height; int index = 0; char *rgba = new char[width * height * 4]; unsigned char *ybase = static_cast<unsigned char *>(videoFrame.yBuffer); unsigned char *ubase = static_cast<unsigned char *>(videoFrame.uBuffer);; unsigned char *vbase = static_cast<unsigned char *>(videoFrame.vBuffer);; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { //YYYYYYYYUUVV u_char Y = ybase[x + y * width]; u_char U = ubase[y / 2 * width / 2 + (x / 2)]; u_char V = vbase[y / 2 * width / 2 + (x / 2)]; int r = static_cast<int>(Y + 1.402 * (V - 128)); if (r > 255) { r = 255; } if (r < 0) { r = 0; } int g = static_cast<int>(Y - 0.34413 * (U - 128) - 0.71414 * (V - 128)); if (g > 255) { g = 255;} if (g < 0) { g = 0; } int b = static_cast<int>(Y + 1.772 * (U - 128)); if (b > 255) { b = 255; } if (b < 0) { b = 0; } rgba[index++] = static_cast<char>(r); //R rgba[index++] = static_cast<char>(g); //G rgba[index++] = static_cast<char>(b); //B rgba[index++] = static_cast<char>(255); } } jbyte buf[width * height * 4]; int i = 0; for (i = 0; i < width * height * 4; i++) { buf[i] = rgba[i]; } jbyteArray jarrRV = env->NewByteArray(width * height * 4); env->SetByteArrayRegion(jarrRV, 0, width * height * 4, buf); env->CallVoidMethod(jobj, jSendMethodId, jarrRV, width, height, videoFrame.rotation); env->DeleteLocalRef(jarrRV);
人臉識別主要使用的是MLKit,經過Firebase便可簡單配置使用,在上一個環節中,咱們把源數據經過jni傳到了java層,如今咱們須要將它轉化成bitmap對象而後傳給MLKit中提供的VisionFaceDetector。
val bitmap = Bitmap.createBitmap(color,width,height,Bitmap.Config.ARGB_8888) //裸數據還須要進行旋轉和水平翻轉 val matrix = Matrix() matrix.postRotate(rotation.toFloat()) matrix.postScale(-1.0f, 1.0f) val rotationBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true) val image = FirebaseVisionImage.fromBitmap(rotationBitmap) val detect = FirebaseVision.getInstance().getVisionFaceDetector(highAccuracyOpts) detect.detectInImage(image) .addOnSuccessListener { val leftEye = face.getLandmark(FirebaseVisionFaceLandmark.LEFT_EYE) val rightEye = face.getLandmark(FirebaseVisionFaceLandmark.RIGHT_EYE) val nose = face.getLandmark(FirebaseVisionFaceLandmark.NOSE_BASE) //獲取到左眼、右眼和鼻子的位置 val leftEyeNose = euclidean(leftEye,nose)//計算鼻子到左眼的距離 val rightEyeNode = euclidean(rightEye,nose)//計算鼻子到右眼的距離 val ratio = min(leftEyeNose,rightEyeNose) / max(leftEyeNose,rightEyeNose) if (ratio > 0.7 && ratio < 1) { //左右眼離鼻子的比例在0.7-1.0之間咱們認爲沒有轉頭 FaceState.FRONT } else { if (rightHalfFace > leftHalfFace) { //右邊眼睛到鼻子距離大於左邊的,咱們認爲轉向了左邊 FaceState.LEFT } else { //反之右邊 FaceState.RIGHT } } }
實現了轉頭識別後,配合上UI和動畫,咱們就可使遊戲中的人偶跟隨咱們的轉頭方向運動了。
因爲遊戲是在兩端同時進行的,因此咱們須要進行端對端的數據傳遞,咱們採用的是聲網提供的消息傳輸方案。經過實時傳遞遊戲過程當中的指令,對雙方遊戲畫面進行控制,傳遞的指令包括:遊戲開始,遊戲結束,向左轉頭,向右轉頭,沒有轉頭以及實時分數等。
//發送方 streamId = rtcEngine.createDataStream(true, true) rtcEngine.sendStreamMessage(streamId, "left".toByteArray()) //接收方 object : IRtcEngineEventHandler override fun onStreamMessage(uid: Int, s: Int, data: ByteArray?) { data?.let { val string = String(it) when (string) { "left" -> { //處理遊戲 } "right"->{ //處理遊戲 } ..... } }
《拿頭玩》這個項目是一個起點,基於它的框架,其實能夠快速地添加到各類app中,造成一個額外的小遊戲模塊。將「接鍋」「甩鍋」的替換成「接優惠券」、「採集素材」等不一樣元素,能夠擴展它的使用場景。經過提供更多有趣的包裝,能夠有效實現社交裂變引流。