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

news2024/12/25 22:17:57

作者简介

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渲染的画面编码成音视频文件。鉴于篇幅限制,文中的流程只截取了部分关键步骤的代码,欢迎大家来交流音视频相关的知识。

【推荐阅读】

  • 瘦身50%-70%,携程 Taro 小程序样式 Size 缩减方案

  • Flutter 地图在携程的最佳实践

  • 携程火车票iOS项目开发体验优化实践

  • 携程机票App KMM iOS工程配置实践

67118c2794171a28944be64c62941950.jpeg

 “携程技术”公众号

  分享,交流,成长

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1191623.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

无线测温系统在电厂的必要性,保障电力系统稳定运行

安科瑞电气股份有限公司 上海嘉定 201801 摘要&#xff1a;采集关键电力设备接电的实时温度&#xff0c;克服有线温度监测系统存在的诸如线路多&#xff0c;布线复杂&#xff0c;维护困难等不足&#xff0c;将无线无源传感器与Zigbee无线通信技术相结合&#xff0c;将物联网技…

Python中的Socket编程

目录 一、概述 二、Socket的基本概念 三、Python中的Socket编程 四、Socket的高级功能 1、多路复用&#xff08;multiplexing&#xff09;&#xff1a; 2、非阻塞式IO&#xff1a; 3、SSL加密&#xff1a; 4、服务端编程&#xff1a; 五、Socket编程的常见问题及解决方…

计算机考研精炼1000题:笔试面试必备攻略

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 ⭐️ 好书推荐 计算机考…

亚马逊云科技Zero ETL集成全面可用,可运行近乎实时的分析和机器学习

亚马逊云科技数据库、数据分析和机器学习全球副总裁Swami Sivasubramanian曾指出&#xff1a;“数据是应用、流程和商业决策的核心。”如今&#xff0c;客户常用的数据传输模式是建立从Amazon Aurora到Amazon Redshift的数据管道。这些解决方案能够帮助客户获得新的见解&#x…

案例研究:利用合成数据提高对象检测性能

通常很难提前知道是否可以在不尝试的情况下生成与真实图像足够相似的图像。但好消息是&#xff1a;尝试起来很容易&#xff01;我们将向您展示如何操作。 本指南是关于生成合成数据的系列文章的一部分。我们还提供指南&#xff0c;可让您使用以下工具生成合成数据&#xff1a;…

剖析WPF模板机制的内部实现

剖析WPF模板机制的内部实现 众所周知&#xff0c;在WPF框架中&#xff0c;Visual类是可以提供渲染&#xff08;render&#xff09;支持的最顶层的类&#xff0c;所有可视化元素&#xff08;包括UIElement、FrameworkElment、Control等&#xff09;都直接或间接继承自Visual类。…

JavaScript的大分水岭:CommonJS vs ES模块

所周知&#xff0c;JavaScript社区喜欢进行热烈的辩论。四年来&#xff0c;我们如何组织代码的问题上一直存在一个分歧——这是一个基本但令人意外地有争议的问题&#xff0c;继续将开发者分开。 这种分歧围绕着 CommonJS 和 ES 模块&#xff0c;这是两个用于划分 JavaScript代…

不用开会员就能在线编辑、管理及分享各类地理空间数据!

「四维轻云」作为一款地理空间数据云管理平台&#xff0c;具有三维模型、正射影像、激光点云、数字高程模型、人工模型和矢量数据等地理空间数据的在线管理、浏览及分享等功能&#xff0c;致力于为用户提供更加方便、快捷的地理空间数据解决方案。 一、发布、管理超大空间数据…

交易所开发搭建

在当今的数字货币市场中&#xff0c;交易所开发搭建已经成为了一个重要的领域。交易所是数字货币交易的主要场所&#xff0c;为投资者提供了安全、可靠、高效的交易服本务文。将详细介绍交易所开发搭建的整个流程&#xff0c;包括需求分析、设计、技术选型、开发、测试和上线等…

Yolov8模型训练报错:torch.cuda.OutOfMemoryError

最近在使用自己的数据训练Yolov8模型的时候遇到了很多错误&#xff0c;下面将逐一解答。 问题报错 在训练过程中红字报错&#xff1a;torch.cuda.OutOfMemoryError: CUDA out of memory. 后面还会跟着一大段报错&#xff1a; Tried to allocate XXX MiB (GPU 0; XXX GiB to…

epoll实现 IO复用

1、epoll实现 IO复用 epoll的提出--》它所支持的文件描述符上限是系统可以最大打开的文件的数目&#xff1b;eg&#xff1a;1GB机器上&#xff0c;这个上限10万个左右。 每个fd上面有callback(回调函数)函数&#xff0c;只有活跃的fd才有主动调用callback&#xff0c;不需要轮询…

国密算法SSL证书

国密算法&#xff0c;即国家商用密码算法&#xff0c;是中国政府推动的一项密码算法标准&#xff0c;目的是提高我国信息安全水平。这一标准覆盖了对称密码算法、非对称密码算法、哈希函数等多个方面。在SSL证书领域&#xff0c;国密算法的应用对于保障网络通信的安全至关重要。…

YB1205B S0T23开关式异步升压具恒压恒流LED驱动器

YB1205B S0T23开关式异步升压具恒压恒流LED驱动器 产品简介&#xff1a; YB1205B是一种输入电压范围宽(0.85.5V),可调恒定电流和限定电流两种模式来驱动白光LED而设计的升压型DCDC变换器。采用变频模式&#xff0c;逐周期限流&#xff0c;使输入输出电流随电源电压降低均匀变…

全局前置路由守卫(beforeEach)

全局前置路由守卫&#xff08;beforeEach&#xff09; 功能&#xff1a;每一次切换任意路由组件之前都会被调用&#xff0c;相当于在进入另一个路由组件之前设置一个权限。 路由守卫的存在意义就是在不同的时间&#xff0c;不同的位置&#xff0c;去添加代码。如&#xff1a;J…

招聘信息采集

首先&#xff0c;我们需要使用PHP的curl库来发送HTTP请求。以下是一个基本的示例&#xff1a; <?php // 初始化curl $ch curl_init();// 设置代理 curl_setopt($ch, CURLOPT_PROXY, "jshk.com.cn");// 设置URL curl_setopt($ch, CURLOPT_URL, "http://www…

echarts图从隐藏到显示以后大小有问题的解决方法

大家好&#xff0c;我是南宫。 今天分享一个刚刚解决的问题。 稍微介绍一下问题的背景&#xff1a; 我有一个绘制柱状图的需求&#xff0c;之前已经画好了&#xff0c;没想到今天对接数据的时候发现&#xff0c;如果没有数据&#xff0c;后端是直接返回一个空数组的。&#…

面向对象高级

本期对应知识库&#xff1a;&#xff08;持续更新中&#xff01;&#xff09; 面向对象高级 (yuque.com) ​​​​​​​尚硅谷_宋红康_对象内存解析.pptx static 适用于公用变量 开发中&#xff0c;变量 经常把一些常量设置为静态static 例如 PI 方法 经常把工具类中的方…

RapidSSL证书

RapidSSL是一家经验丰富的证书颁发机构&#xff0c;主要专注于提供标准和通配符SSL证书的域验证SSL证书。在2017年被DigicertCA收购后&#xff0c;RapidSSL改进了技术并开始使用现代基础设施。专注于为小型企业和网站提供基本安全解决方案的SSL加密。RapidSSL它具有强大的浏览器…

股票四倍杠杆什么意思?

股票四倍杠杆是指投资者通过借款或使用金融衍生品&#xff0c;以增加其投资股票的能力&#xff0c;达到放大投资回报的目的。具体来说&#xff0c;投资者可以通过向券商或银行等金融机构借入资金&#xff0c;或者使用融资融券等金融衍生品&#xff0c;以增加其购买股票的资本&a…

SAM + 用于文本到图像修复的稳定扩散

推荐基于稳定扩散(stable diffusion) AI 模型开发的自动纹理工具&#xff1a; DreamTexture.js自动纹理化开发包 - NSDT 什么是SAM&#xff1f; 今年早些时候&#xff0c;Meta AI 发布了新的开源项目&#xff1a;Segment Anything Model &#xff08;SAM&#xff09;&#xff…