干货 | 移动端使用OpenGL转场特效的音视频合成应用

Android音视频合成,OpenGL视频转场效果 处理音频的时候,由于每个视频出现的位置不一样,每个视频原声什么时候开始播放也不一样,所以音画同步很重要,每个视频开始时间点的偏移量要跟音频同步,并且还要混合bgm,所以还是选用ffmpeg来实现。可以看出很明显的OpenGL对于性能的提升,2.0版本是进行了优化并且没有丰富的转场效果前提下才达到这个性能,3.0使用OpenGL做出了比较多的转场效果,可参考。这里的效果主要是转场效果,视频之间的转场,ffmpeg没有转场的功能命令,所以需要自己用骚操作实现,但这样会导致大量的性能损耗,这一点之前的文章说过。 阅读详情

作者简介

jzg,携程资深前端开发工程师,专注Android开发;

zx,携程高级前端开发工程师,专注iOS开发;

zcc,携程资深前端开发工程师,专注iOS开发。

前言

近年来短视频的火爆,让内容创作类的APP获得了巨大的流量。用户通过这类工具编辑自己的短视频,添加各式各样的炫酷特效,从而呈现出更加丰富多彩的视频内容。本文将会介绍如何使用移动端原生API,将图片添加转场特效并且最终合成为视频的基本流程。

一、音视频基础知识

我们经常会和视频打交道,最常见的就是MP4格式的视频。这样的视频其实一般是由音频和视频组成的音视频容器。下面先会介绍音视频相关概念,为音视频技术的应用作一个铺垫,希望能对音视频频开发者提供一些帮助。

1.1 视频的基础知识

1.1.1 视频帧

视频中的一个基本概念就是帧,帧用来表示一个画面。视频的连续画面就是由一个个连续的视频帧组成。

1.1.2  帧率

帧率,FPS,全称Frames Per Second。指每秒传输的帧数,或者每秒显示的帧数。一般来说,帧率影响画面流畅度,且成正比:帧率越大,画面越流畅;帧率越小,画面越有跳动感。一个较权威的说法:当视频帧率不低于24FPS时,人眼才会觉得视频是连贯的,称为“视觉暂留”现象。16FPS可以达到一定的满意程度,但效果略差。因此,才有说法:尽管帧率越高越流畅,但在很多实际应用场景中24FPS就可以了(电影标准24FPS,电视标准PAL制25FPS)。

1.1.3 分辨率

分辨率,Resolution,也常被俗称为图像的尺寸或者图像的大小。指一帧图像包含的像素的多少,常见有1280x720(720P),1920X1080(1080P)等规格。分辨率影响图像大小,且与之成正比:分辨率越高,图像越大;反之,图像越小。

1.1.4 码率

码率,BPS,全称Bits Per Second。指每秒传送的数据位数,常见单位KBPS(千位每秒)和MBPS(兆位每秒)。码率是更广泛的(视频)质量指标:更高的分辨率,更高的帧率和更低的压缩率,都会导致码率增加。

1.1.5 色彩空间

通常说的色彩空间有两种:

RGB:RGB的颜色模式应该是我们最熟悉的一种,在现在的电子设备中应用广泛。通过R、G、B三种基础色,可以混合出所有的颜色。

YUV:YUV是一种亮度与色度分离的色彩格式,三个字母的意义分别为:

Y:亮度,就是灰度值。除了表示亮度信号外,还含有较多的绿色通道量。单纯的Y分量可以显示出完整的黑白图像。

U:蓝色通道与亮度的差值。

V:红色通道与亮度的差值。

其中,U、V分量分别表示蓝(blue)、红(red)分量信号,只含有色度信息,所以YUV也称为YCbCr,其中,Cb、Cr的含义等同于U、V,C可以理解为component或者color。

RGB和YUV的换算

YUV与RGB相互转换的公式如下(RGB取值范围均为0-255):

Y = 0.299R + 0.587G + 0.114BU = -0.147R - 0.289G + 0.436BV = 0.615R - 0.515G - 0.100B R = Y + 1.14VG = Y - 0.39U - 0.58VB = Y + 2.03U

1.2 音频的基础知识

音频数据的承载方式最常用的是脉冲编码调制,即PCM。

1.2.1 采样率和采样位数

采样率是将声音进行数字化的采样频率,采样位数与记录声波振幅有关,位数越高,记录的就越准确。

1.2.2 声道数

声道数,是指支持能不同发声(注意是不同声音)的音响的个数。

1.2.3 码率

码率,是指一个数据流中每秒钟能通过的信息量,单位bps(bit per second)。

码率 = 采样率 * 采样位数 * 声道数

上面介绍的音视频的数据还需要进行压缩编码,因为音视频的数据量都非常大,按照原始数据保存会非常的耗费空间,而且想要传输这样庞大的数据也很不方便。其实音视频的原始数据中包含大量的重复数据,特别是视频,一帧一帧的画面中包含大量的相似的内容。所以需要对音视频数据进行编码,以便于减小占用的空间,提高传输的效率。

1.3 视频编码

通俗地理解,例如一个视频中,前一秒画面跟当前的画面内容相似度很高,那么这两秒的数据是不是可以不用全部保存,只保留一个完整的画面,下一个画面看有哪些地方有变化了记录下来,拿视频去播放的时候就按这个完整的画面和其他有变化的地方把其他画面也恢复出来。记录画面不同然后保存下来这个过程就是数据编码,根据不同的地方恢复画面的过程就是数据解码。

一般常见的视频编码格式有H26x系列和MPEG系列。
H26x(1/2/3/4/5)系列由ITU(International Telecommunication Union)国际电传视讯联盟主导。
MPEG(1/2/3/4)系列由MPEG(Moving Picture Experts Group, ISO旗下的组织)主导。

H264是新一代的编码标准,以高压缩高质量和支持多种网络的流媒体传输著称。iOS 8.0及以上苹果开放了VideoToolbox框架来实现H264硬编码,开发者可以利用VideoToolbox框架很方便地实现视频的硬编码。

H264编码的优势:

  • 低码率

  • 高质量的图像 

  • 容错能力强

  • 网络适应性强

H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍。举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1。

1.4 音频编码

和视频编码一样,音频也有许多的编码格式,如:WAV、MP3、WMA、APE、FLAC等等。

AAC

  • AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式

  • 特点:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码

  • 使用场合:128Kbit/s以下的音频编码,多用于视频中音频轨的编码

WAV

  • 在PCM数据格式的前面加上44字节,描述PCM的采样率、声道数、数据格式等信息,不会压缩

  • 特点:音质好,大量软件支持

  • 使用场合:多媒体开发的中间文件、保存音乐和音效素材

MP3

  • 使用LAME编码

  • 特点:音质在128kbit/s以上表现不错,压缩比较高,大量软件硬件都支持,兼容性好

  • 使用场合:高比特率(传输效率 bps, 这里的b是位,不是比特)对兼容性有要求的音乐欣赏

OGG

  • 特点:可以用比MP3更小的码率实现比MP3更好的音质,高中低码率下均有良好的表现

  • 不足:兼容性不够好,流媒体特性不支持

  • 适合场景:语音聊天的音频消息场景

APE

  • 无损压缩

FLAC

  • 专门针对PCM音频的特点设计的压缩方式,而且可以使用播放器直接播放FLAC压缩的文件

  • 免费,支持大多数操作系统

二、使用OpenGL的底层转场特效和原生平台硬编码进行图片、音乐、转场合成视频需要哪些 API

2.1 Android端和使用流程及相关API介绍

如果想要给图片添加转场特效并且合成为视频,需要使用OpenGL对图片进行渲染,搭配自定义的转场着色器,先让图片"动起来"。然后使用MediaCodec将画面内容进行编码,然后使用MediaMuxer将编码后的内容打包成一个音视频容器文件。

2.1.1 Mediacodec

MediaCodec是从API16后引入的处理音视频编解码的类,它可以直接访问Android底层的多媒体编解码器,通常与MediaExtractor,MediaSync, MediaMuxer,MediaCrypto,MediaDrm,Image,Surface,以及AudioTrack一起使用。

下面是官网提供的MediaCodec工作的流程图:

4e8905f9b7b061adbb265adf98281e3d.png

我们可以看到左边是input,右边是output。这里要分两种情况来讨论:

1)利用MediaCodec进行解码的时候,输入input是待解码的buffer数据,输出output是解码好的buffer数据。

2)利用MediaCodec进行编码的时候,输入input是一个待编码的数据,输出output是编码好的buffer数据。

val width = 720
    val height = 1280
    val bitrate = 5000
    val encodeType = "video/avc"
    //配置用于编码的MediaCodec
    val mCodec = MediaCodec.createEncoderByType(encodeType)
    val outputFormat = MediaFormat.createVideoFormat(encodeType, width, height)
    outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
    outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, DEFAULT_ENCODE_FRAME_RATE)
    outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
    outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        outputFormat.setInteger(
            MediaFormat.KEY_BITRATE_MODE,
            MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ
        )
    }
    codec.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    //这一步很关键,这一步得到的surface后面将会用到
    val mSurface = codec.createInputSurface()
    mCodec.start()
    val mOutputBuffers = mCodec.outputBuffers
    val mInputBuffers = mCodec.inputBuffers

以上是MediaCodec的作为编码器的基本配置,其中MediaCodec.createInputSurface()这个方法可以为我们创建一个用于向MediaCodec进行输入的surface。这样通过MediaCodec就能获取到编码后的数据了。用这样的方式编码我们不需要向MedaiCodec输入待编码的数据,MediaCodec会自动将输入到surface的数据进行编码。

2.1.2 EGL环境

OpenGL是一组用来操作GPU的API,但它并不能将绘制的内容渲染到设备的窗口上,这里需要一个中间层,用来作为OpenGL和设备窗口之间的桥梁,并且最好是跨平台的,这就是EGL,是由Khronos Group提供的一组平台无关的API。

OpenGL绘制的内容一般都是呈现在GLSurfaceView中的(GLSurfaceView的surface),如果我们需要将内容编码成视频,需要将绘制的内容渲染到MediaCodec提供的Surface中,然后获取MediaCodec输出的编码后的数据,封装到指定的音视频文件中。

创建EGL环境的主要步骤如下:

//1,创建 EGLDisplay
val mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
// 2,初始化 EGLDisplay
val version = IntArray(2)
EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)
// 3,初始化EGLConfig,EGLContext上下文
val config :EGLConfig? = null
if (mEGLContext === EGL14.EGL_NO_CONTEXT) {
    var renderableType = EGL14.EGL_OPENGL_ES2_BIT
        val attrList = intArrayOf(
            EGL14.EGL_RED_SIZE, 8,
            EGL14.EGL_GREEN_SIZE, 8,
            EGL14.EGL_BLUE_SIZE, 8,
            EGL14.EGL_ALPHA_SIZE, 8,
            EGL14.EGL_RENDERABLE_TYPE, renderableType,
            EGL14.EGL_NONE, 0,
            EGL14.EGL_NONE
        )
        //配置Android指定的标记
        if (flags and FLAG_RECORDABLE != 0) {
            attrList[attrList.size - 3] = EGL_RECORDABLE_ANDROID
            attrList[attrList.size - 2] = 1
        }
        val configs = arrayOfNulls<EGLConfig>(1)
        val numConfigs = IntArray(1)


        //获取可用的EGL配置列表
        if (!EGL14.eglChooseConfig(mEGLDisplay, attrList, 0,
                configs, 0, configs.size,
                numConfigs, 0)) {
            configs[0]
        }
    val attr2List = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
    val context = EGL14.eglCreateContext(
        mEGLDisplay, config, sharedContext,
        attr2List, 0
    )
    mEGLConfig = config
    mEGLContext = context
}
//这里还需要创建一个EGL用于输出的surface,这里的参数就可以传入上一小节介绍的利用MeddiaCodec创建的Surface
 fun createWindowSurface(surface: Any): EGLSurface {
        val surfaceAttr = intArrayOf(EGL14.EGL_NONE)


        val eglSurface = EGL14.eglCreateWindowSurface(
                                        mEGLDisplay, mEGLConfig, surface,
                                        surfaceAttr, 0)


        if (eglSurface == null) {
            throw RuntimeException("Surface was null")
        }


        return eglSurface
    }

配置EGL环境后,还要一个surface作为输出,这里就是要利用MediaCodec创建的surface作为输出,即EGL的输出作为MediaCodec的输入。

2.1.3 MediaMuxer

MediaMuxer是Android平台的音视频合成工具,上面我们介绍了MediaCodec可以编码数据,EGL环境可以让OpenGL程序将绘制的内容渲染到MediaCodec中,MediaCodec将这些数据编码,最后这些编码后的数据需要使用MediaMuxer写入到指定的文件中。

MediaMuxer基本使用:

//创建一个MediaMuxer,需要指定输出保存的路径,和输出保存的格式。
val mediaMuxer = MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
//根据MediaFormat添加媒体轨道
mediaMuxer.addTrack(MediaFormat(...))
//将输入的数据,根据指定的轨道保存到指定的文件路径中。
muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
//结合上面的所说的使用MediaCodec获取到已编码的数据
//当前帧的信息
var mBufferInfo = MediaCodec.BufferInfo()
//编码输出缓冲区
var mOutputBuffers: Array<ByteBuffer>? = null
//获取到编码输出的索引,写到指定的保存路径
val index = mCodec.dequeueOutputBuffer(mBufferInfo, 1000)
muxer.writeSampleData(currentTrackIndex,mOutputBuffers[index],mBufferInfo)

2.1.4 MediaExtractor

MediaExtractor是Android平台的多媒体提取器,能够根据视频轨道或者音频轨道去提取对应的数据。在进行视频编辑时,可以利用MediaExtractor来提取指定的音频信息,封装到目标音视频文件中。

//根据指定文件路径创建MediaExtractor
 val mediaExtractor = MediaExtractor(...)
 //为MediaExtractor选择好对应的媒体轨道
 mediaExtractor.selectTrack(...)
 //读取一帧的数据
 val inputBuffer = ByteBuffer.allocate(...)
 mediaExtractor.readSampleData(inputBuffer, 0)
 //进入下一帧
 mediaExtractor.advance()
 //MediaExtractor读取到的音频数据可以使用MediaMuxer的writeSampleData方法写入到指定的文件中

以上就是利用Android平台的硬编码相关API,将OpenGL渲染到画面编码成视频的基本流程介绍。

三、iOS端合成流程及相关API使用

由于AVFoundation原生框架对于图层特效处理能力有限,无法直接生成和写入多张图片之间切换的转场效果,所以需要自行对图片和音乐按照时间线,去实现音视频数据从解码到转场特效应用,以及最终写入文件的整个流程。

那么在多张图片合成视频的过程中,核心的部分就是如何处理多张图片之间的转场效果。这个时候我们需要配合OpenGL底层的特效能力,自定义滤镜将即将要切换的2张图片通过片元着色器生成新的纹理。本质就是在这两个纹理对象上去实现纹理和纹理之间的切换,通过Mix函数混合两个纹理图像,使用time在[0,1]之间不停变化来控制第二个图片纹理混合的强弱变化从而实现渐变效果。接下来开始介绍合成的流程和具体API的使用。

3.1 音视频基础API

在合成的过程中,我们使用到了AVAssetWriter这个类。AVAssetWriter可以将多媒体数据从多个源进行编码(比如接下来的多张图片和一个BGM进行合成)并写入指定文件格式的容器中,比如我们熟知的MPEG-4文件。

3.1.1 AVAssetWriter 与AVAssetWriterInput

AVAssetWriter通常由一个或多个AVAssetWriterInput对象构成,将AVAssetWriterInput配置为可以处理指定的多媒体类型,比如音频或视频,用于添加将包含要写入容器的多媒体数据的CMSampleBufferRef对象。同时因为asset writer可以从多个数据源写入容器,因此必须要为写入文件的每个track(即音频轨道、视频轨道)创建一个对应的AVAssetWriterInput对象。

AVAssetWriterInput可以设置视频的主要参数如输出码率,帧率,最大帧间隔,编码方式,输出分辨率以及填充模式等。也可以设置音频的主要参数如采样率,声道,编码方式,输出码率等。

3.1.2 CMSampleBufferRef 与AVAssetWriterInputPixelBufferAdaptor

CMSampleBuffer是一个基础类,用于处理音视频管道传输中的通用数据。CMSampleBuffer中包含零个或多个某一类型如音频或者视频的采样数据。可以封装音频采集后、编码后、解码后的数据(PCM数据、AAC数据)以及视频编码后的数据(H.264数据)。而CMSampleBufferRef是对CMSampleBuffer的一种引用。在提取音频的时候,像如下的使用方式同步复制输出的下一个示例缓冲区。

CMSampleBufferRef sampleBuffer = [assetReaderAudioOutput copyNextSampleBuffer];

每个AVAssetWriterInput期望以CMSampleBufferRef对象形式接收数据,如果在处理视频样本的数据时,便要将CVPixelBufferRef类型对象(像素缓冲样本数据)添加到asset writer input,这个时候就需要使用AVAssetWriterInputPixelBufferAdaptor 这个专门的适配器类。这个类在附加被包装为CVPixelBufferRef对象的视频样本时提供最佳性能。

AVAssetWriterInputPixelBufferAdaptor它是一个输入的像素缓冲适配器,作为assetWriter的视频输入源,用于把缓冲池中的像素打包追加到视频样本上。在写入文件的时候,需要将CMSampleBufferRef转成CVPixelBuffer,而这个转换是在CVPixelBufferPool中完成的。AVAssetWriterInputPixelBufferAdaptor的实例提供了一个CVPixelBufferPool,可用于分配像素缓冲区来写入输出数据。使用它提供的像素缓冲池进行缓冲区分配通常比使用额外创建的缓冲区更加高效。

CVPixelBufferRef pixelBuffer = NULL;
    CVPixelBufferPoolCreatePixelBuffer(NULL, self.inputPixelBufferAdptor.pixelBufferPool,&pixelBuffer);

每个AVAssetWriterInputPixelBufferAdaptor都包含一个assetWriterInput,用于接收缓冲区中的数据,并且AVAssetWriterInput有一个很重要的属性readyForMoreMediaData,来标识现在缓冲区中的数据是否已经处理完成。通过判断这个属性,我们可以向AVAssetWriterInputPixelBufferAdaptor中添加数据(appendPixelBuffer:)以进行处理。

if(self.inputPixelBufferAdptor.assetWriterInput.isReadyForMoreMediaData) {
    BOOL success = [self.inputPixelBufferAdptor appendPixelBuffer:newPixelBuffer withPresentationTime:self.currentSampleTime];    


    if (success) {
        NSLog(@"append buffer success");
    }
}

3.1.3 设置输入输出参数,以及多媒体数据的采样

第一步:创建AVAssetWriter对象,传入生成视频的路径和格式

AVAssetWriter *assetWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:outFilePath] fileType:AVFileTypeMPEG4 error:&outError];

第二步:设置输出视频相关信息,如大小,编码格式H264,以及创建视频的输入类videoWriterInput,以便后续给assetReader添加videoWriterInput。

CGSize size = CGSizeMake(480, 960);


NSDictionary *videoSetDic = [NSDictionary dictionaryWithObjectsAndKeys:AVVideoCodecTypeH264,AVVideoCodecKey,
[NSNumber numberWithInt:size.width],AVVideoWidthKey,[NSNumber numberWithInt:size.height],AVVideoHeightKey,nil];


AVAssetWriterInput *videoWriterInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSetDic];


//将读取的图片内容添加到assetWriter                                              
if ([assetWriter canAddInput:videoWriterInput]) {
    [assetWriter addInput:videoWriterInput];
}

第三步:创建一个处理视频样本时专用的适配器对象,这个类在附加被包装为CVPixelBufferRef对象的视频样本时能提供最优性能。如果想要将CVPixelBufferRef类型对象添加到asset writer input,就需要使用AVAssetWriterInputPixelBufferAdaptor类。

NSDictionary *pixelBufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA],kCVPixelBufferPixelFormatTypeKey,nil];


AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput pixelBufferAttributes:pixelBufferAttributes];

第四步:音频数据的采集、添加音频输入

//创建音频资源
AVURLAsset *audioAsset = [[AVURLAsset alloc] initWithURL:audioUrl options:nil];
//创建音频Track
AVAssetTrack *assetAudioTrack = [audioAsset tracksWithMediaType:AVMediaTypeAudio].firstObject;
//创建读取器 
AVAssetReader *assetReader = [[AVAssetReader alloc] initWithAsset:audioAsset error:&error];
//读取音频track中的数据
NSDictionary *audioSettings = @{AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM]};


AVAssetReaderTrackOutput *assetReaderAudioOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:assetAudioTrack outputSettings: audioSettings];
//向接收器添加assetReaderAudioOutput输出
if ([assetReader canAddOutput:assetReaderAudioOutput]) {
    [assetReader addOutput:assetReaderAudioOutput];
}


//音频通道数据,设置音频的比特率、采样率的通道数
AudioChannelLayout acl;
bzero( &acl, sizeof(acl));
acl.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;


NSData *channelLayoutAsData = [NSData dataWithBytes:&acl length:offsetof(AudioChannelLayout, acl)];


NSDictionary *audioSettings = @{AVFormatIDKey:[NSNumber numberWithUnsignedInt:kAudioFormatMPEG4AAC],AVEncoderBitRateKey:[NSNumber numberWithInteger:128000], AVSampleRateKey:[NSNumber numberWithInteger:44100], AVChannelLayoutKey:channelLayoutAsData,AVNumberOfChannelsKey : [NSNumber numberWithUnsignedInteger:2]};
//创建音频的assetWriterAudioInput,将读取的音频内容添加到assetWriter
AVAssetWriterInput *assetWriterAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:[assetAudioTrack mediaType] outputSettings: audioSettings];


if ([assetWriter canAddInput:assetWriterAudioInput]) {
    [assetWriter addInput:assetWriterAudioInput];
}


//Writer开始进行写入流程
[assetWriter startSessionAtSourceTime:kCMTimeZero];

3.2 转场切换效果中的图片处理

上面介绍了音视频合成的大致流程,但是核心的部分是在于我们在合成视频时,如何去写入第一张和第二张图片展示间隙中的切换过程效果。这个时候就得引入GPUImage这个底层框架,而GPUImage是iOS端对OpenGL的封装。即我们通过继承GPUImageFilter去实现自定义滤镜,并重写片元着色器的效果,通过如下代理回调得到这个过程中返回的一系列处理好的纹理样本数据。

-(void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates;

然后转换成相应的pixelBuffer数据,通过调用appendPixelBuffer:添加到帧缓存中去,从而写入到文件中。

-(BOOL)appendPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime;

3.2.1 如何自定义滤镜

在GPUImageFilter中默认的着色器程序比较简单,只是简单的进行纹理采样,并没有对像素数据进行相关操作。所以在自定义相关滤镜的时候,我们通常需要自定义片段着色器的效果来处理纹理效果从而达到丰富的转场效果。

我们通过继承GPUImageFilter来自定义我们转场效果所需的滤镜,首先是创建一个滤镜文件compositeImageFilter继承于GPUImageFilter,然后重写父类的方法去初始化顶点和片段着色器。

- (id)initWithVertexShaderFromString:(NSString *)vertexShaderString fragmentShaderFromString:(NSString *)fragmentShaderString;

这个时候需要传入所需的片元着色器代码,那么怎么自定义GLSL文件呢,以下便是如何编写具体的GLSL文件,即片元着色器实现代码。传入纹理的顶点坐标textureCoordinate、2张图片的纹理imageTexture、imageTexture2,通过mix函数混合两个纹理图像,使用time在[0,1]之间不停变化来控制第二个图片纹理混合的强弱变化从而实现渐变效果。

precision highp float;
varying highp vec2 textureCoordinate;
uniform sampler2D imageTexture;
uniform sampler2D imageTexture2;
uniform mediump vec4 v4Param1;
float progress = v4Param1.x;
void main()
{
    vec4 color1 = texture2D(imageTexture, textureCoordinate);
    vec4 color2 = texture2D(imageTextur2, textureCoordinate);
    gl_FragColor = mix(color1, color2, step(1.0-textureCoordinate.x,progress));
}

3.2.2 了解GPUImageFilter中重点API

在GPUImageFilter中有三个最重要的API,GPUImageFilter会将接收到的帧缓存对象经过特定的片段着色器绘制到即将输出的帧缓存对象中,然后将自己输出的帧缓存对象传给所有Targets并通知它们进行处理。方法被调用的顺序:

1)生成新的帧缓存对象

- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex;

2)进行纹理的绘制  

- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates;

3)绘制完成通知所有的target处理下一帧的纹理数据 

- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime;

通过如上代理回调就可以得到这个过程中返回的一系列处理好的纹理样本数据。

按照方法调用顺序,我们一般先重写newFrameReadyAtTime方法,构建最新的顶点坐标,生成新的帧缓存对象。

- (void)newFrameReadyAtTime:(CMTime)frameTime atIndex:(NSInteger)textureIndex {
    static const GLfloat imageVertices[] = {
        -1.0f, -1.0f,
        1.0f, -1.0f,
        -1.0f,  1.0f,
        1.0f,  1.0f,
    };
    [self renderToTextureWithVertices:imageVertices textureCoordinates:[[self class] textureCoordinatesForRotation:inputRotation]];
}

然后在这个方法中调用renderToTextureWithVertices去绘制所需的纹理,并获取到最终的帧缓存对象。以下是部分核心代码:

- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates {
glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE5);
glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);
glUniform1i(filterInputTextureUniform, 5);
glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, [self adjustVertices:vertices]);
glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glFinish();
CVPixelBufferRef pixel_buffer = NULL;
CVReturn status = CVPixelBufferPoolCreatePixelBuffer(NULL, [self.videoPixelBufferAdaptor pixelBufferPool], &pixel_buffer);
if ((pixel_buffer == NULL) || (status != kCVReturnSuccess)) {
    CVPixelBufferRelease(pixel_buffer);
    return;
} else {
    CVPixelBufferLockBaseAddress(pixel_buffer, 0);
    GLubyte *pixelBufferData = (GLubyte *)CVPixelBufferGetBaseAddress(pixel_buffer);
    glReadPixels(0, 0, self.sizeOfFBO.width, self.sizeOfFBO.height, GL_RGBA, GL_UNSIGNED_BYTE, pixelBufferData);
CVPixelBufferUnlockBaseAddress(pixel_buffer, 0);
        }
    }
}

3.2.3 pixel_buffer的写入

在上述的处理过程当中,我们便获取到了所需的帧缓存样本数据pixel_buffer。而这个数据便是合成转场切换过程中的数据,我们把它进行写入,自此便完成了第一张和第二张图片转场效果效果的写入。待转场效果写入之后,我们便可按照此流程根据时间的进度写入第二张图片以及后续的第二张图片和第三张图片的转场效果。依此类推,一直到写完所有的图片。

CVPixelBufferLockBaseAddress(pixelBuffer, 0);
if (self.assetWriter.status != AVAssetWriterStatusWriting) {
    [self.assetWriter startWriting];
}
[self.assetWriter startSessionAtSourceTime:frameTime];
if (self.assetWriter.status == AVAssetWriterStatusWriting) {
    if (CMTIME_IS_NUMERIC(frameTime) == NO)  {
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
        return;
    }
    //确定写操作是否已完成、失败或已取消
    if ([self.videoPixelBufferAdaptor appendPixelBuffer:pixelBufferwithPresentationTime:frameTime]) {                                                    
        NSLog(@"%f", CMTimeGetSeconds(frameTime));
    }
}else {
    NSLog(@"status:%d", self.assetWriter.status);
    }
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);

以上便是在iOS端处理音视频合成的具体步骤,难点在于如何使用GPUImage去实现复杂的转场效果并将其写到到容器中。

本文介绍了音视频相关的基本知识,让大家对音视频的关键概念有了一些理解。然后分别介绍了Android和iOS这两个移动平台音视频编解码API,利用这些平台自带的API,我们可以将OpenGL渲染的画面编码成音视频文件。鉴于篇幅限制,文中的流程只截取了部分关键步骤的代码,欢迎大家来交流音视频相关的知识。

【推荐阅读】

67118c2794171a28944be64c62941950.jpeg

 “携程技术”公众号

  分享,交流,成长

java版opencv之Javacv各种场景使用案例 文章目录1.关于Javacv~~2. [官网下载最新OpenCV4.8](https://opencv.org/releases/),并解压~~ *不一定要安装opencv*~~3. 将opencv的jar包及动态库dll文件引入项目~~4.pom引入javacv库5.测试5.1 图片美颜5.2 图片人脸检测5.3 提取视频中的语音5.4 音视频剪辑5.5 录屏5.6 推流与流媒体播放 [参考](https://xinchen.blog.youkuaiyun.com/article/details/1214349 阅读详情

相关推荐

OpenGL ES:配合MediaCodec硬解码渲染(视频加滤镜播放)

MediaCodec 解码后的视频格式为yuv,而OpenGL所能渲染的格式为rgb,因此我们需要使用扩展库中的扩展纹理 GLES11Ext.GL_TEXTURE_EXTERNAL_OES 而它的作用就是实现YUV格式到RGB的自动转化。 片段着色器中需使用扩展采样器: uniform samplerExternalOES sTexture 原理步骤: 生成一个oes纹理,并且以此得到一个S...

ikata 4424

常用知识之CMSampleBufferRef系

// // ViewController.m // Demo // // Created by on 2021/9/28. // #import "ViewController.h" #import <CoreMedia/CoreMedia.h> @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; .

hbblzjy的博客 2907

OpenGL使用入门笔记(一)

OpenGL使用入门笔记(一)1 几个基本函数和概念了解1.1一个入门的OpenGL程序1.2一个进阶的绘图程序解释一下函数什么意思1.3 GLUT函数及解释1.4 走样与反走样啥意思2 绘制几何模型2.1 画点2.2 画线2.2.1 线段2.2.2 线段序列2.2.3 封闭线段2.3 画三角形2.3.1 三角形2.3.2 三角形序列2.4 画四边形2.4.1 四边形2.4.1 四边形条带2.5 ...

luolei188的博客 4564

AVFoundation - AVAssetReader & Writer

如果你仅仅想从一个或多个tracks中读取媒体数据,并可能会把数据转换成另一种格式,就使用AVAssetReaderTrackOutput类,为你想要从asset中读取的每个AVAssetTrack对象,使用一个单独的track ouput对象。在创建asset reader之后,至少设置一个output来接收读取的媒体数据。AVAssetWriterInput被配置为可以处理指定的媒体类型,比如音频或视频,并且附加在其后的样本会在最终输出时生成一个独立的AVAssetTrack。

AI工程化、开源分享、文档翻译、代码笔记 768

JavaCV开发详解之34:使用filter滤镜实现字符滚动和无限循环滚动字符叠加,跑马灯特效制作

springmvc防xss脚本注入攻击,springmvc过滤html和js标签,html和js标签转义一、前言二、原理三、springmvc如何实现xss过滤3.1、xss转义包装器3.2 xss过滤器3.3 web.xml配置过滤器 一、前言 xss脚本注入攻击大家应该经常见了,不多说了,直接讲防xss脚本注入的原理吧。 二、原理 原理很简单,http后端通过过滤器过滤request接受的参数内容,把包含"<“和”>","&"、"/“和“””的字符进行转义即可。 例如: 普通注入脚

eguid音视频方向博客,分享经过实践检验的硬核音视频技术。FFmpeg、openCV和JavaCV音视频和图像处理系列教程发布十周年,众多开发者的严格验证。欢迎关注,一起交流探索学习最新音视频和图像处理技术 1584

Android 图片转场和轮播特效,你想要的都在这了

未经作者(微信ID:Byte-Flow)允许,禁止转载使用 OpenGL 做图像的转场效果或者图片轮播器,可以实现很多令人惊艳的效果。

字节流动 3万+

OpenGL基础知识介绍和简单使用

OpenGL(全写Open Graphics Library)是指定义了一个跨编程语言、跨平台的编程接口规格的专业的图形程序接口。它用于三维图像(二维的亦可),是一个功能强大,调用方便的底层图形库。OpenGL在不同的平台上有不同的实现,但是它定义好了专业的程序接口,不同的平台都是遵照该接口来进行实现的,思想完全相同,方法名也是一致的,所以使用时也基本一致,只需要根据不同的语言环境稍有不同而已。

zxy_de_android的博客 6756

国产射频直采收发器CX8242KA JESD204C调试

杭州诚芯公司的CX8242KA是国内开发的极少数支持JESD204C接口的高速AD、DA,Serdes具备支持接收和发送,并且速率可以到25Gbps,在国内属于领先水平。250M参考时钟输入,AD采样率2.5G,DA采样率10G,3收2发,主通道模式4,反馈通道模式1,基带采样率1250M,线速率20.625G,收发同时配置混频625M。CX8242KA的204C接口模式支持10种模式,在手册上都有说明。2、 支持TX信道编码,RX信道解码,提升健壮性。这里使用模式4,支持双通道模式,并且反馈开启。

RFSOC的博客 6219

解决取出音轨[[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] 报错的问题

今天合成视频的时候遇到了这个问题,报错的内容是 -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array 出错的代码是 [AudioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, item.asset.duration) ofTrack:[[item.a

woshiwls的专栏 5449

从零到英雄:WSL2与Ubuntu22.04的终极性能调优指南

本文详细介绍了如何在Windows系统中通过WSL2与Ubuntu22.04进行性能调优,涵盖内核定制、GPU加速、网络优化等关键技巧,帮助开发者打造媲美原生Linux的高性能开发环境,特别适合AI训练和大数据处理等资源密集型任务。

weixin_29218509的博客 523

python实现语音信号的分帧、加窗、预处理

语音信号实现分帧、加窗、预处理音频文件读取预加重分帧加窗 音频文件读取 librosa是非常强大的python语音信号处理库。 读取音频: 使用语句librosa.load(path, sr=22050, mono=True, offset=0.0, duration=None)读取音频文件,默认的采样率是22050,若要保留音频的原始采样率,使用sr=None。 其中: path:表示音频文件的路径 sr:表示采样率 mono:bool,是布尔类型,表示是否将信号转换为单声道 offset:float,后

weixin_45272908的博客 1万+

AVAssetReader和AVAssetWriter

AVAssetReader和AVAssetWriterAVAssetReader和AVAssetWriter类可以让开发者直接处理媒体样本AVAssetReaderAVAssetReader用于从AVAssert实例中读取媒体样本。每个AVAssetReader对象在某个时刻只能和单个asset关联,但这个asset可包含多个tracks。所以,在开始读取之前,必须给asset reader指定一个

了凡 8578

【VMware篇】7-通过ISO镜像升级VMware Esxi

本文主要介绍Dell服务器上安装的ESXi升级的方法之一(采用ISO升级)。

chen_yunan的博客 5512

【我的OpenGL学习进阶之旅】学习OpenGL ES 3.0 的实战 Awsome Demo (上)

一个超棒的OpenGL ES 学习实践项目 https://github.com/ouyangpeng/OpenGLESDemo

字节卷动 5534

Vivado工程芯片型号更换全流程:从IP更新到生成新bit文件(避坑指南)

本文详细介绍了Vivado工程中更换芯片型号的全流程,包括IP更新、约束文件适配和生成新bit文件的关键步骤。特别针对FPGA开发中常见的兼容性问题提供避坑指南,帮助开发者高效完成芯片迁移,确保设计稳定性和性能优化。

weixin_42549451的博客 297

嵌入式开发实战:LWIP中PPP协议配置全流程(含CHAP认证避坑指南)

本文是一份嵌入式开发实战指南,详细解析了如何在STM32平台上配置LWIP协议栈中的PPP协议,实现从串口到IP网络的完整连接。内容涵盖工程搭建、源码整合、关键配置、串口驱动绑定、状态管理以及CHAP认证等核心流程,并重点提供了CHAP认证失败的排查与避坑指南,帮助开发者高效打通嵌入式设备的网络连接。

plant的博客 461

IOS OpenGL ES GPUImage 图像阈值边缘检测GPUImageThresholdEdgeDetectionFilter

GPUImage共125个滤镜,分为四类31filters,颜色处理相关40filters,图像处理相关.29filters,混合模式相关.25filters,视觉效果相关.属于GPUImage图像视觉效果相关,用于图像阈值边缘检测。//@Author猿说编程//@Blog(个人博客地址)//@File//@Time30//@Motto不积跬步无以至千里,不积小流无以成江海,程序人生的精彩需要坚持不懈地积累!({...

猿说编程 949

Matlab可调用MODTRAN5封装器之Mod5

MODTRAN 是一个计算地球大气中电磁辐射传输的软件代码。MODTRAN是通过使用具有严格格式的输入文本文件来控制的。这个文件被称为“tape5”,扩展名为.tp5或.ltn。手工创建或编辑这个文件是不方便的。Mod5提供了一组函数来简化MODTRAN用例的处理。这些方法包括将tape5文件格式读取到Mod5类“实例”或“对象”中,更改文件中描述的案例实例,将tape5格式写回文件,在案例上运行MODTRAN并将MODTRAN输出文件中的各种结果读取回Matlab。还有许多绘图方法可用。

qq_19512613的博客 3254

Android OpenGL ES 学习(十二) - MediaCodec + OpenGL 解析H264视频+滤镜

OpenGL 学习教程前面我们学习了OpenGL是如何渲染 YUV 视频的,这一章,我们让OpenGL 与 MediaCodec 结合,实现解析 H264 文件,并实现抖音效果。效果如下:MediaCodec 为Android 的硬编,在一些快速解码设备,我们都是使用MediaCodec,如果你对 MediaCodec 如何解码不熟悉,可以先阅读。

夏至的稻穗的博客 5518

从零开始:手把手教你设计人形机器人锂电池供电系统

人形机器人要像人类一样“活起来”,必须有稳定的“能量血液”——供电系统。本文聚焦锂电池供电系统如何根据机器人功耗选择合适的锂电池参数(容量、电压、放电倍率);如何设计电池管理系统(BMS)保护电池安全;如何解决机器人运动时的“瞬时大电流”供电难题;如何通过硬件电路和代码实现供电系统的监测与控制。用“机器人小助手的能量危机”故事引入锂电池供电系统;拆解锂电池核心参数(容量、电压、放电倍率)和供电系统三大组件(电池组、BMS、电源转换模块);

AI天才研究院 1758

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

iOS开发--AVFoundation视频合成(添加背景音乐)

前言 当下音视频处理的技术的重要性不言而喻, FFmpeg的强大不可撼动, 但是, 确实难啃. 由简递难, 我们来看看苹果原生的API如何做到对视频素材进行处理的. AVFoundation既可以对视频进行播放, 同时也可以对视频素材进行处理, 此类合成共分为三章: 视频合成 音视频的合成 视频和多音频轨道的合成 难道算是递增吧, 理论方向底层处理是一样的. AVMutableComposition 合成工具 AVMutableCompositionTrack 合成工具中的轨道 AVAssetTrac

张福润的博客 8736

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

【我的OpenGL学习进阶之旅】学习OpenGL ES 3.0 的实战 Awsome Demo (下)之 图片转场和轮播特效专场

前面两篇文章 【我的OpenGL学习进阶之旅】学习OpenGL ES 3.0 的实战 Awsome Demo (上) 【我的OpenGL学习进阶之旅】学习OpenGL ES 3.0 的实战 Awsome Demo (中) 介绍了一下项目和部分案例效果展示,这篇接着介绍其他的案例效果展示。 【我的OpenGL学习进阶之旅】学习OpenGL ES 3.0 的实战 Awsome Demo (下)之 图片转场和轮播特效专场 一、介绍 最近在学习OpenGL ES 3.0 相关技术,写了一个Demo

字节卷动 4136

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

OpenGL入门指南:构建图形应用的基础知识

OpenGL入门指南:构建图形应用的基础知识

yaosichengalpha的博客 810

音视频-视频编/解码 实战

先来简单看下 音视频的采集 。 一、音视频的采集 音视频采集的核心流程: 音/视频采集 用到的视频输出的类是AVCaptureVideoDataOutput,音频输出的类是AVCaptureAudioDataOutput。 采集成功后的代理方法输出的音视频对象为CMSampleBufferRef类型的sampleBuffer。这里我们可以使用AVCaptureConnection来判断是音频还是视频。 - (void)captureOutput:(AVCaptureOutp...

翻肚鱼儿的博客 1605

NDK OpenGL仿抖音极快极慢录制特效视频

3)录制我们在另外一个线程中进行(录制线程),所以录制的EGL环境和显示的EGL环境(GLSurfaceView,显示线程)是两个独立的工作环境,他们又能够共享上下文资源:显示线程中使用的texture等,需要能够在录制线程中操作 (通过录制线程中使用OpenGL绘制到MediaCodec的Surface)。以上接口创建一个Surface,然后我们在这个Surface中作画(输入源的直接绑定),MediaCodec就能够自动的编码Surface中的画作,我们只需要从输出缓冲区取出编码完成之后的数据。

sziitjin的博客 787

Android小视频录制技术实现

老大下达了一个要求,让我高仿一个类似微信录制小视频的功能,根据网上各路大神的demo及思路参考下,然后我就粗糙的做了一个小demo,个人感觉还不错,不喜者忽喷。源码链接在文件后面提供,欢迎大家学习及参考,有不足之处请跟我联系,我会加紧改进,一起探讨学习,希望能帮助那些需要实现类似功能的小伙伴们。话不多说,直接上效果图:主界面图短视频界面图短视频录制效果图        不能发视频效果,只能是图片,就

u013334392的博客 5978
上一篇: 干货 | 携程后台低代码平台的探究与实践
下一篇: 干货 | 高效联动,携程机票IVR可视化的探索和实践
携程技术
博客等级 码龄10年 3716粉丝 324原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值