MediaPipe + FFmpeg生成绿幕视频的另一种方式

前言

之前在Android使用MediaPipe + FFmpeg生成绿幕视频一文中,将视频通过MediaPipe人像分割能力将视频中的人像位置输出,然后将其余部分的像素修改成绿色,保存成每一帧的图片,最后通过FFmpeg逐帧合成成视频。这个方法比较粗糙,主要还是因为每一帧都需要经过IO,时间和空间都需要比较大的要求 。所以本文想通过深度使用FFmpeg将IO这一部分优化掉。我们利用FFmpeg的编解码能力,再利用OpenGL将逐帧处理,最后输出一个绿幕视频。

观前提醒:

  1. 本文关于FFmpeg和OpenGL的部分均参考Android 音视频开发打怪升级系列文章。如果对FFmpeg和OpenGL不熟悉的同学,可以优先阅读
  2. 由于本文是直接通过在C++层调用FFmpeg及OpenGL的API,所以代码模版基于与上述系列文章的一致,所以本文只介绍有关生成绿幕视频的关键部分

本文原创只在掘金发布!!!

FFmpeg编译

关于FFmpeg的编译内容,网上也有很多文章可以参考,这里笔者将编译脚本贴出。使用的是FFmpeg6.1由于需要输出mp4格式,所以这里还需要x264。

ps:正常视频还需要音频的编解码,所以其实还需要fdk-aac,本文暂时忽略

  • x264:
bash 复制代码
#!/bin/bash
 
export NDK=/自己的路径/Library/Android/sdk/ndk/21.4.7075529 #NDK path
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64
export API=21 #对应android sdk版本 
 
function build_android
{
cd x264

APP_ABI=$1
  echo "======== > Start build $APP_ABI"
  case ${APP_ABI} in
  armeabi-v7a)
    HOST=armv7a-linux-android
    CROSS_PREFIX=$TOOLCHAIN/bin/arm-linux-androideabi-
    ;;
  arm64-v8a)
    HOST=aarch64-linux-android
    CROSS_PREFIX=$TOOLCHAIN/bin/aarch64-linux-android-
    ;;
  esac
 
./configure \
	--prefix=$PREFIX \
	--disable-cli \
	--disable-shared \
	--enable-pic \
	--enable-static \
	--enable-strip \
	--cross-prefix=$CROSS_PREFIX \
	--sysroot=$SYSROOT \
	--host=$HOST
 
make clean
 
make -j4
 
make install

cd ..
}
 
PREFIX=`pwd`/libs/x264/armeabi-v7a
SYSROOT=$TOOLCHAIN/sysroot
export TARGET=armv7a-linux-androideabi
export CC=$TOOLCHAIN/bin/$TARGET$API-clang
export CXX=$TOOLCHAIN/bin/$TARGET$API-clang++
 
build_android armeabi-v7a
 
PREFIX=`pwd`/libs/x264/arm64-v8a
export TARGET=aarch64-linux-android
export CC=$TOOLCHAIN/bin/$TARGET$API-clang
export CXX=$TOOLCHAIN/bin/$TARGET$API-clang++
 
build_android arm64-v8a
  • FFmpeg:
bash 复制代码
#!/bin/bash
# 用于编译android平台的脚本
 
# NDK所在目录
NDK_PATH=/自己的路径/Library/Android/sdk/ndk/21.4.7075529 # tag1
# macOS 平台编译,其他平台看一下 $NDK_PATH/toolchains/llvm/prebuilt/ 下的文件夹名称
HOST_PLATFORM=darwin-x86_64  #tag1
# minSdkVersion
API=21
 
TOOLCHAINS="$NDK_PATH/toolchains/llvm/prebuilt/$HOST_PLATFORM"
SYSROOT="$NDK_PATH/toolchains/llvm/prebuilt/$HOST_PLATFORM/sysroot"
# 生成 -fpic 与位置无关的代码
CFLAG="-D__ANDROID_API__=$API -Os -fPIC -DANDROID "
LDFLAG="-lc -lm -ldl -llog "
 
# 输出目录
PREFIX=`pwd`/libs/ffmpeg
# 日志输出目录
CONFIG_LOG_PATH=${PREFIX}/log
# 公共配置
COMMON_OPTIONS=
# 交叉配置
CONFIGURATION=
 
build() {
  APP_ABI=$1
  CFLAG264=-I/自己的路径/x264/$APP_ABI/include
  LDFLAG264=-L/自己的路径/x264/$APP_ABI/lib
  echo "======== > Start build $APP_ABI"
  echo "PKG_CONFIG_PATH:${PKG_CONFIG_PATH}"
  case ${APP_ABI} in
  armeabi-v7a)
    ARCH="arm"
    CPU="armv7-a"
    MARCH="armv7-a"
    TARGET=armv7a-linux-androideabi
    CC="$TOOLCHAINS/bin/$TARGET$API-clang"
    CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
    LD="$TOOLCHAINS/bin/$TARGET$API-clang"
 
    # 交叉编译工具前缀
    CROSS_PREFIX="$TOOLCHAINS/bin/arm-linux-androideabi-"
    EXTRA_CFLAGS="$CFLAG -mfloat-abi=softfp -mfpu=vfp -marm -march=$MARCH "
    EXTRA_LDFLAGS="$LDFLAG"
    EXTRA_OPTIONS="--enable-neon --cpu=$CPU "
    ;;
  arm64-v8a)
    ARCH="aarch64"
    TARGET=$ARCH-linux-android
    CC="$TOOLCHAINS/bin/$TARGET$API-clang"
    CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
    LD="$TOOLCHAINS/bin/$TARGET$API-clang"
    CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
    EXTRA_CFLAGS="$CFLAG"
    EXTRA_LDFLAGS="$LDFLAG"
    EXTRA_OPTIONS=""
    ;;
  x86)
    ARCH="x86"
    CPU="i686"
    MARCH="i686"
    TARGET=i686-linux-android
    CC="$TOOLCHAINS/bin/$TARGET$API-clang"
    CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
    LD="$TOOLCHAINS/bin/$TARGET$API-clang"
    CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
    #EXTRA_CFLAGS="$CFLAG -march=$MARCH -mtune=intel -mssse3 -mfpmath=sse -m32"
    EXTRA_CFLAGS="$CFLAG -march=$MARCH  -mssse3 -mfpmath=sse -m32 "
    EXTRA_LDFLAGS="$LDFLAG"
    EXTRA_OPTIONS="--cpu=$CPU "
    ;;
  x86_64)
    ARCH="x86_64"
    CPU="x86-64"
    MARCH="x86_64"
    TARGET=$ARCH-linux-android
    CC="$TOOLCHAINS/bin/$TARGET$API-clang"
    CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
    LD="$TOOLCHAINS/bin/$TARGET$API-clang"
    CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
    #EXTRA_CFLAGS="$CFLAG -march=$CPU -mtune=intel -msse4.2 -mpopcnt -m64"
    EXTRA_CFLAGS="$CFLAG -march=$CPU -msse4.2 -mpopcnt -m64 "
    EXTRA_LDFLAGS="$LDFLAG"
    EXTRA_OPTIONS="--cpu=$CPU "
    ;;
  esac
 
  echo "-------- > Start clean workspace"
 
make clean
 
  echo "-------- > Start build configuration"
  CONFIGURATION="$COMMON_OPTIONS"
  CONFIGURATION="$CONFIGURATION --logfile=$CONFIG_LOG_PATH/config_$APP_ABI.log"
  CONFIGURATION="$CONFIGURATION --prefix=$PREFIX"
  CONFIGURATION="$CONFIGURATION --libdir=$PREFIX/libs/$APP_ABI"
  CONFIGURATION="$CONFIGURATION --incdir=$PREFIX/includes/$APP_ABI"
  CONFIGURATION="$CONFIGURATION --pkgconfigdir=$PREFIX/pkgconfig/$APP_ABI"
  CONFIGURATION="$CONFIGURATION --cross-prefix=$CROSS_PREFIX"
  CONFIGURATION="$CONFIGURATION --arch=$ARCH"
  CONFIGURATION="$CONFIGURATION --sysroot=$SYSROOT"
  CONFIGURATION="$CONFIGURATION --cc=$CC"
  CONFIGURATION="$CONFIGURATION --cxx=$CXX"
  CONFIGURATION="$CONFIGURATION --ld=$LD"
  # nm 和 strip
  CONFIGURATION="$CONFIGURATION --nm=$TOOLCHAINS/bin/llvm-nm"
  CONFIGURATION="$CONFIGURATION --strip=$TOOLCHAINS/bin/llvm-strip"
  CONFIGURATION="$CONFIGURATION $EXTRA_OPTIONS"
 
  echo "-------- > Start config makefile with $CONFIGURATION --extra-cflags=${EXTRA_CFLAGS}${CFLAG264} --extra-ldflags=${EXTRA_LDFLAGS}${LDFLAG264}"
 
  ./configure ${CONFIGURATION} \
  --extra-cflags="$EXTRA_CFLAGS$CFLAG264" \
  --extra-ldflags="$EXTRA_LDFLAGS$LDFLAG264" \
  --pkg-config="pkg-config --static" \
  --pkg-config-flags=--static
 
  echo "-------- > Start make $APP_ABI with -j1"
  make -j1
  echo "-------- > Start install $APP_ABI"
  make install
  echo "++++++++ > make and install $APP_ABI complete."
}
 
build_all() {
  cd ffmpeg-6.1

  #配置开源协议声明
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-gpl"
  #目标android平台
  COMMON_OPTIONS="$COMMON_OPTIONS --target-os=android"
  #取消默认的静态库
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-static"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-shared"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-protocols"
  #开启交叉编译
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-cross-compile"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-optimizations"
  COMMON_OPTIONS="$COMMON_OPTIONS --disable-debug"
  #尽可能小
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-small"
  COMMON_OPTIONS="$COMMON_OPTIONS --disable-doc"
  #不要命令(执行文件)
  COMMON_OPTIONS="$COMMON_OPTIONS --disable-programs"  # do not build command line programs
  COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffmpeg"    # disable ffmpeg build
  COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffplay"    # disable ffplay build
  COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffprobe"   # disable ffprobe build
  COMMON_OPTIONS="$COMMON_OPTIONS --disable-symver"
  COMMON_OPTIONS="$COMMON_OPTIONS --disable-network"
  COMMON_OPTIONS="$COMMON_OPTIONS --disable-x86asm"
  COMMON_OPTIONS="$COMMON_OPTIONS --disable-asm"
  #启用
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-pthreads"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-mediacodec"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-jni"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-zlib"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-pic"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-libx264"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-encoder=libx264"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-encoder=h264"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-muxer=flv"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-encoder=h264"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=mpeg4"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=mjpeg"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=png"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=vorbis"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=opus"
  COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=flac"
  echo "COMMON_OPTIONS=$COMMON_OPTIONS"
  echo "PREFIX=$PREFIX"
  echo "CONFIG_LOG_PATH=$CONFIG_LOG_PATH"
  mkdir -p ${CONFIG_LOG_PATH}
  
  build "armeabi-v7a"
  build "arm64-v8a"
  #build "x86"
  #build "x86_64"

  cd ..
}
 
echo "-------- Start --------"
build_all
echo "-------- End --------"

可能遇到的问题:

  1. ERROR: x264 not found using pkg-config 编译FFmpeg时,可能会报x264 not found using pkg-config,可以尝试配置一下x264的pkgconfig环境变量
bash 复制代码
# export PKG_CONFIG_PATH=/自己的路径/x264/armeabi-v7a/lib/pkgconfig:/自己的路径/x264/arm64-v8a/lib/pkgconfig:$PKG_CONFIG_PATH
  1. 华为鸿蒙4.0遇到无法找到编码器问题 笔者在使用编译好的FFmpeg动态库在华为MatePad11时,遇到了类似的问题:鸿蒙4.0 手机视频编解码时报错,4.0以下的可以正常执行-华为开发者论坛

这个问题暂时无解,实测在其他设备上没有遇到。

绿幕视频生成模型

整个生成流程如图

  • 三个流程都运行在独立线程
  • 人像切割在解码线程中调用,保证解码数据与人像切割的分类数据一一对应

MediaPipe接入

由于FFmpeg的编解码直接套用了上述推荐文章的模版,这里就直接跳过了,直接进入MediaPipe的使用部分。由于MediaPipe官方只提供了Java API,所以这里只能通过JNI层调用Java的形式了。

kotlin 复制代码
object PortraitCuttingManager {
    private val imagesegmenter: ImageSegmenter by lazy {
        val baseOptions = BaseOptions.builder()
            .setDelegate(Delegate.CPU)
            .setModelAssetPath("selfie_segmenter.tflite")
            .build()

        val options = ImageSegmenter.ImageSegmenterOptions.builder()
            .setRunningMode(RunningMode.VIDEO)
            .setBaseOptions(baseOptions)
            .setOutputCategoryMask(true)
            .setOutputConfidenceMasks(false)
            .build()

        return@lazy ImageSegmenter.createFromOptions(BaseApp.application, options)
    }

    private var pixel: IntArray? = null

    @JvmStatic
    fun segment(frame: ByteBuffer, timestampMs: Long, width: Int, height: Int): IntArray {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            val mpImage = ByteBufferImageBuilder(frame, width, height, IMAGE_FORMAT_RGBA).build()
            try {
                val result = imagesegmenter.segmentForVideo(mpImage, timestampMs)
                val newImage = result.categoryMask().get()
                val resultByteBuffer = ByteBufferExtractor.extract(newImage)

                val pixels = if (this.pixel != null) this.pixel else {
                    this.pixel = IntArray(resultByteBuffer.capacity())
                    this.pixel
                }

                for (index in pixels!!.indices) {
                    // Using unsigned int here because selfie segmentation returns 0 or 255U (-1 signed)
                    // with 0 being the found person, 255U for no label.
                    // Deeplab uses 0 for background and other labels are 1-19,
                    // so only providing 20 colors from ImageSegmenterHelper -> labelColors
                    pixels[index] = if (resultByteBuffer.get(index).toUInt() > 0U) 1 else 0
                }
                return pixels
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        return intArrayOf()
    }
}

在Java层定义了一个单例,用于接入MediaPipe。可以看到在上述的segment方法中

  • frame: ByteBuffer表示当前帧的数据
  • timestampMs: Long表示当前帧对应的时间
  • width,height表示的是当前帧对应的宽高

接入的逻辑和上一篇大同小异。只是在最后输出的数组中,将非人像部分处理成1,人像部分处理成0,提供给C++层使用。

C++ 复制代码
class PortraitCuttingBridge {
private:
    const char *TAG = "PortraitCuttingBridge";

    jclass portraitCuttingManagerClass = NULL;

    int *portraitCuttingResult = NULL;

public:
    PortraitCuttingBridge(JNIEnv *env);

    ~PortraitCuttingBridge();

    void segment(JNIEnv *env, OneFrame *oneFrame);

    int *getLastResult();

    void release();
};

在C++层定义了一个PortraitCuttingBridge,专门用于对接上述的单例。

C++ 复制代码
PortraitCuttingBridge::PortraitCuttingBridge(JNIEnv *env) {
    jclass portraitCuttingManagerClass = env->FindClass(
            "me/xcyoung/ffmpeg_test/PortraitCuttingManager");
    if (!portraitCuttingManagerClass) {
        return;
    }

    //  因为存在多线程,所以这里的句柄要声明成全局
    this->portraitCuttingManagerClass = (jclass) env->NewGlobalRef(portraitCuttingManagerClass);
}

这里需要注意的是,构造时就需要将PortraitCuttingManager对应的句柄声明成全局 ,这是由于整个过程在C++层,这里面的解码、绘制、编码均拥有独立线程运行,并不在Java层的调用线程 。所以需要在初始化(Java层的调用线程)时将句柄获取到,以免ClassLoader不一致导致无法找到该类

C++ 复制代码
void PortraitCuttingBridge::segment(JNIEnv *env, OneFrame *oneFrame) {
    jmethodID segmentMethod = env->GetStaticMethodID(
        portraitCuttingManagerClass, "segment", "(Ljava/nio/ByteBuffer;JII)[I");

    // 获取AVFrame中的图像数据
    uint8_t *imageData = oneFrame->data;
    // RGBA 所以需要*4
    int imageSize = oneFrame->width * oneFrame->height * 4;

    // 创建ByteBuffer,并将AVFrame的图像数据复制到其中
    jobject byteBuffer = env->NewDirectByteBuffer(imageData, imageSize);

    // 调用 segment 方法
    jintArray result = static_cast<jintArray>(env->CallStaticObjectMethod(
            portraitCuttingManagerClass,
            segmentMethod,
            byteBuffer,
            oneFrame->pts,
            oneFrame->width,
            oneFrame->height));
    // 获取整数数组的长度
    jsize length = env->GetArrayLength(result);

    // 获取整数数组的指针
    jint* intArrayElements = env->GetIntArrayElements(result, nullptr);

    // 创建C++中的整数数组并将数据复制到其中
    if (this->portraitCuttingResult == NULL) {
        this->portraitCuttingResult = new int[length];
    }
    
    for (int i = 0; i < length; ++i) {
        this->portraitCuttingResult[i] = static_cast<int>(intArrayElements[i]);
    }

    // 释放整数数组的指针
    env->ReleaseIntArrayElements(result, intArrayElements, JNI_ABORT);
}

int *PortraitCuttingBridge::getLastResult() {
    return this->portraitCuttingResult;
}
  • OneFrame是通过解码后生成的自定义类型。实际上FFmpeg解码后会生成AVFrame

视频解码:LearningVideo/app/src/main/cpp/media/decoder/video/v_decoder.cpp

C++ 复制代码
sws_scale(m_sws_ctx, frame->data, frame->linesize, 0,
          height(), m_rgb_frame->data, m_rgb_frame->linesize);
OneFrame *one_frame = new OneFrame(m_rgb_frame->data[0],
                                   m_rgb_frame->linesize[0],
                                   frame->pts,
                                   time_base(),
                                   frame->width,
                                   frame->height,
                                   NULL,
                                   false);
                                   
m_video_render->Render(one_frame);

ps:这里在解码后会将视频的yuv转换成rgb,然后实例了一个OneFrame进行渲染。

  • 通过上述获取到的单例句柄,获取到segment的方法句柄进行调用。这里的方法调用就和上述的Java层对应上了。
  • 返回结果后,会保存到一个全局数组变量中,这里做的目的是避免频繁申请内存地址导致OOM。
  • getLastResult方法会获取到最近一次识别完的结果。

最后MediaPipe的人像分割会在每一帧渲染时调用,Render方法对应上述的m_video_render->Render(one_frame);

C++ 复制代码
void PortraitCuttingDrawer::Render(OneFrame *one_frame) {
    if (portraitCuttingBridge != NULL) {
        this->portraitCuttingBridge->segment(m_env, one_frame);
    }
    cst_data = one_frame->data;
}

OpenGL处理像素

经过了上述的解码、识别后,就会经过OpenGL将这一帧进行渲染。此时会存在两个数据:

  1. 解码得到的视频帧数据
  2. 通过MediaPipe识别的分类数据

这里笔者的做法是将两组数据当作两个纹理传递给着色器,于是顶点着色器和片元着色器就长这样(ps:这里使用的是OpenGL ES2.0):

  • 顶点着色器
glsl 复制代码
attribute vec4 aPosition;
attribute vec2 aCoordinate;
varying vec2 vCoordinate;
void main() {
    gl_Position = aPosition;
    vCoordinate = aCoordinate;
}
  • 片元着色器
glsl 复制代码
#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform sampler2D uTexture;
uniform sampler2D boolArray;
varying vec2 vCoordinate;

void main() {
    float v = texture2D(boolArray, vCoordinate).r;
    gl_FragColor = (v == 0.0) ? texture2D(uTexture, vCoordinate) : vec4(0.0, 1.0, 0.0, 1.0);
}

重点看片元着色器,boolArray表示人像切割输出的分类数据,由于分类数据只是一维所以这里只取r这一位。如果为0代表是人像,所以这里输出原视频对应的颜色,否则输出绿色

解码后视频帧数据的纹理设置

C++ 复制代码
//激活指定纹理单元
glActiveTexture(GL_TEXTURE0);
//绑定纹理ID到纹理单元
glBindTexture(GL_TEXTURE_2D, m_texture_id);
//将活动的纹理单元传递到着色器里面
glUniform1i(m_texture_handler, 0);
//配置边缘过渡参数
glTexParameterf(type, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(type, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(type, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(type, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

glTexImage2D(GL_TEXTURE_2D, 0,
             GL_RGBA,
             origin_width(), origin_height(),
             0,
             GL_RGBA,
             GL_UNSIGNED_BYTE,
             cst_data);

这里的cst_data即为最近一次解码出来的视频帧

C++ 复制代码
//激活指定纹理单元
glActiveTexture(GL_TEXTURE1);
//绑定纹理ID到纹理单元
glBindTexture(GL_TEXTURE_2D, m_portrait_cutting_texture_id);
//将活动的纹理单元传递到着色器里面
glUniform1i(m_portrait_cutting_texture_handler, 0);
//配置边缘过渡参数
glTexParameterf(type, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(type, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(type, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(type, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

unsigned char *data = reinterpret_cast<unsigned char *>(portraitCuttingResult);
glTexImage2D(GL_TEXTURE_2D, 0,
             GL_RGBA,
             origin_width(), origin_height(),
             0,
             GL_RGBA,
             GL_UNSIGNED_BYTE,
             data);

portraitCuttingResult即为最近一次人像切割的分类数据,需要注意的是这里需要将其强转成unsigned char *

这里就是运用了OpenGL简单的纹理贴图逻辑,其他的诸如顶点坐标、纹理坐标等都是常规操作,这里就不过多介绍了。

总结

本文是上一篇文章Android使用MediaPipe + FFmpeg生成绿幕视频的另一种实现方案。直接使用FFmpeg进行编码优化生成时的IO问题。通过OpenGL逐帧处理,对比操作Bitmap的像素点速度也有所提升。

相关推荐
dvlinker13 小时前
【音视频开发】使用支持硬件加速的D3D11绘图遇到的绘图失败与绘图崩溃问题的记录与总结
音视频开发·c/c++·视频播放·d3d11·d3d11绘图模式
音视频牛哥6 天前
Android平台GB28181实时回传流程和技术实现
音视频开发·视频编码·直播
音视频牛哥7 天前
RTMP、RTSP直播播放器的低延迟设计探讨
音视频开发·视频编码·直播
音视频牛哥11 天前
电脑共享同屏的几种方法分享
音视频开发·视频编码·直播
x007xyz2 个月前
前端纯手工绘制音频波形图
前端·音视频开发·canvas
音视频牛哥2 个月前
Android摄像头采集选Camera1还是Camera2?
音视频开发·视频编码·直播
九酒2 个月前
【harmonyOS NEXT 下的前端开发者】WAV音频编码实现
前端·harmonyos·音视频开发
音视频牛哥2 个月前
结合GB/T28181规范探讨Android平台设备接入模块心跳实现
音视频开发·视频编码·直播
哔哩哔哩技术2 个月前
自研点直播转码核心
音视频开发