
注意:本文 最初发布 于2014年,以Vuforia 4的示例为例。 但是,这里介绍的解决方案是通用的,并且可以应用于所有版本,只需确保您了解幕后情况。
我最近在iOS上进行了一个项目,该项目需要与Vuforia SDK集成。 它是为iOS和Android构建的增强现实专有框架,由于其创新的识别库而非常受欢迎。 吸引广告商或希望在其应用程序中加入商业活动的人们最酷的演示之一涉及在目标上方播放视频的能力。 Vuforia甚至为此提供了示例应用程序。 但是,基于纹理的远程视频流不适用于纹理。
这是一个长期存在的问题,论坛上的人们都在寻求解决方案,有些人提供过时和/或性能不佳的免费解决方案,或者提供非常昂贵的付费解决方案。
视频上的视频渲染过程一般
为了将视频流传输到OpenGL纹理上,必须执行以下操作:
- 初始化可渲染表面。 这是每次会话一次的操作。
- 创建具有ID的纹理。
- 将纹理分配给该表面(应用着色器等)。
- 在每个渲染上,根据识别出的对象的坐标将变换应用于可渲染表面。
- 获取视频字节并将其解码以获取实际的视频数据。
- 将视频数据转换为OpenGL数据(准备绘制)
- 将这些视频数据应用于从步骤2获得的纹理。
Vuforia的样本中已经进行了步骤1-3 。 步骤4也是vuforia SDK存在的原因; 为您提供世界坐标空间内已识别对象的变换和坐标。 因此,样品(以及来自vuforia的所有样品)中还包括步骤4。
步骤5–7是困难的部分,而Vuforia SDK不负责的部分。 这就是我们,第三方开发商的角色。
Vuforia样本的实际问题:
正如我已经提到的,Vuforia的SDK仅负责将物体识别到世界空间中,并为您提供其坐标和变换。 这些信息的处理方式取决于您。 因此,应将Vuforia的VideoPlayback示例作为您可以使用它的示例,而不是看到其局限性。
在示例内部,Vuforia大量使用了AVAssetReader和AVAssetReaderOutput来执行以下操作。 正如许多人在论坛中已经指出的那样,AVASsetReader负责从本地文件URL读取,并且不支持远程文件。 因此,“视频纹理渲染”中的第5步是有问题的,因为您需要将从远程位置获取的视频数据解码为实际的OpenGL数据,然后在屏幕上呈现这些数据。 许多人在论坛上说过,iOS上不可能进行远程纹理渲染。
这离事实还远。
解决方案
我们需要做的是准备好要渲染的实际OpenGL数据,并将这些数据作为纹理应用到Vuforia创建的纹理上。 SDK和示例已经创建了OpenGL坐标系,因此剩下的就是获取OpenGL数据,并从原始示例代码转移数据流。
我们将使用iOS 6中引入的AVPlayerItemVideoOutput而不是AVAssetReader。此类具有方法copyPixelBufferForItemTime:itemTimeForDisplay:,这正是我们要使用的方法,以便将原始OpenGL数据获取到在纹理上渲染。
以下代码示例旨在替换/更新Vuforia的VideoPlayback示例上的相应功能。 代码肯定可以改进。
首先,让我们设置视频播放器和视频输出项,以便以后提取视频缓冲区的内容。
-(BOOL)loadMediaURL:(NSURL *)url
{
BOOL ret = NO;
资产= [[[[AVURLAsset alloc] initWithURL:url选项:无]保留];
如果(零!=资产){
//现在,我们可以尝试加载媒体,因此报告成功。 我们会
//发现加载时实际上是否成功完成加载
//由系统回调
ret =是;
[asset loadValuesAsynchronouslyForKeys:@ [kTracksKey] completeHandler:^ {
//完成处理程序块(加载时在主队列上调度
//完成)
dispatch_async(dispatch_get_main_queue(),^ {
NSError *错误=无;
AVKeyValueStatus状态= [资产statusOfValueForKey:kTracksKey错误:&error];
NSDictionary *设置= @ {(id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA)};
AVPlayerItemVideoOutput * output = [[[[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:settings] autorelease];
self.videoOutput =输出;
如果(状态== AVKeyValueStatusLoaded){
//加载资产,获取信息并准备
//播放
如果(![self prepareAssetForPlayback]){
mediaState =错误;
}
}
其他{
//错误
mediaState =错误;
}
});
}];
}
返回ret
}
调用每个帧后,-updateVideoData负责准备要显示的视频数据。 以下代码是Vuforia的修改后的示例代码,并使用-copyPixelBufferForItemTime:itemTimeForDisplay:来提取流式视频内容,并将其与此时渲染的OpenGL纹理绑定。
//使用最新的可用视频数据更新OpenGL视频纹理
-(GLuint)updateVideoData
{
GLuint textureID = 0;
//如果当前正在播放纹理
如果(PLAYING == mediaState && PLAYER_TYPE_ON_TEXTURE == playerType){
[latestSampleBufferLock锁定];
playerCursorPosition = CACurrentMediaTime()-mediaStartTime;
// self.playerCursorCurrentCMTIME = self.player.currentTime;
// CMTime caCurrentTime = CMTimeMake(self.playerCursorPosition * TIMESCALE,TIMESCALE);
unsigned char * pixelBufferBaseAddress = NULL;
CVPixelBufferRef pixelBuffer;
//如果我们有一个有效的缓冲区,请锁定其像素缓冲区的基地址
//如果((NULL!= LatestSampleBuffer){
// pixelBuffer = CMSampleBufferGetImageBuffer(latestSampleBuffer);
pixelBuffer = [self.videoOutput copyPixelBufferForItemTime:player.currentItem.currentTime itemTimeForDisplay:nil];
CVPixelBufferLockBaseAddress(pixelBuffer,0);
pixelBufferBaseAddress =(无符号字符*)CVPixelBufferGetBaseAddress(pixelBuffer);
//}
//其他{
//没有视频样本缓冲区:我们可能已经被要求
//在任何一个可用之前提供一个,否则我们可能已阅读全部
//可用的框架
// DEBUGLOG(@“没有可用的视频样本缓冲区”);
//}
如果(NULL!= pixelBufferBaseAddress){
//如果尚未创建视频纹理,请立即创建
如果(0 == videoTextureHandle){
videoTextureHandle = [self createVideoTexture];
}
glBindTexture(GL_TEXTURE_2D,videoTextureHandle);
const size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
如果(bytesPerRow / BYTES_PER_TEXEL == videoSize.width){
//解码视频行之间没有填充
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,(GLsizei)videoSize.width,(GLsizei)videoSize.height,0,GL_BGRA,GL_UNSIGNED_BYTE,pixelBufferBaseAddress);
}
其他{
//解码后的视频在行之间包含填充。 我们绝不能
//将其上传到图形内存,因为我们不想显示它
//为纹理分配存储空间(大小正确)
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,(GLsizei)videoSize.width,(GLsizei)videoSize.height,0,GL_BGRA,GL_UNSIGNED_BYTE,NULL);
//现在将每行纹理数据作为子图像上传
for(int i = 0; i <videoSize.height; ++ i){
GLubyte *行= pixelBufferBaseAddress + i * bytesPerRow;
glTexSubImage2D(GL_TEXTURE_2D,0,0,i,(GLsizei)videoSize.width,1,GL_BGRA,GL_UNSIGNED_BYTE,line);
}
}
glBindTexture(GL_TEXTURE_2D,0);
//解锁缓冲区
CVPixelBufferUnlockBaseAddress(pixelBuffer,0);
textureID = videoTextureHandle;
}
如果(pixelBuffer){
CFRelease(pixelBuffer);
}
[latestSampleBufferLock解锁];
}
返回textureID;
}
为了更改视频输出的设置,我们还必须执行其他一些干预。
//准备要播放的AVURLAsset
-(BOOL)prepareAssetForPlayback
{
//获取视频属性
NSArray * videoTracks = [self.asset trackWithMediaType:AVMediaTypeVideo];
AVAssetTrack * videoTrack = videoTracks [0];
self.videoSize = videoTrack.naturalSize;
self.videoLengthSeconds = CMTimeGetSeconds([self.asset持续时间]);
//在时间0.0开始播放
self.playerCursorStartPosition = kCMTimeZero;
//以全音量开始播放(音频混合级别,而不是系统音量)
self.currentVolume = PLAYER_VOLUME_DEFAULT;
//创建资产轨道以供阅读
BOOL ret = [self prepareAssetForReading:self.playerCursorStartPosition];
如果(ret){
//准备AVPlayer播放音频
[自我prepareAVPlayer];
//通知客户资产已准备就绪
self.mediaState =就绪;
}
返回ret
}
//准备要读取的AVURLAsset,以便我们从中获取视频帧数据
-(BOOL)prepareAssetForReading:(CMTime)startTime
{
BOOL ret =是;
// =====音频=====
//获取第一个音轨
NSArray * arrayTracks = [self.asset trackWithMediaType:AVMediaTypeAudio];
如果(0 <[arrayTracks count]){
self.playAudio =是;
AVAssetTrack * assetTrackAudio = arrayTracks [0];
AVMutableAudioMixInputParameters * audioInputParams = [AVMutableAudioMixInputParameters audioMixInputParameters];
[audioInputParams setVolume:self.currentVolume atTime:self.playerCursorStartPosition];
[audioInputParams setTrackID:[assetTrackAudio trackID]];
NSArray * audioParams = @ [audioInputParams];
AVMutableAudioMix * audioMix = [AVMutableAudioMix audioMix];
[audioMix setInputParameters:audioParams];
AVPlayerItem * item = [self.player currentItem];
[item setAudioMix:audioMix];
}
返回ret
}
这些都是可以进行的所有更改,以设置视频播放并渲染以使流的视频纹理化。 但是,Vuforia的样本也必须在许多领域进行更新,以了解现在可以播放远程视频。
-(BOOL)isPlayableOnTexture
{
//我们可以在纹理上渲染本地文件
返回是;
}
而已! 您可能需要做一些较小的更改,但这是使教程运行的基本概念。 此方法论已在Vuforia 4.0中进行了测试,并且效果很好(并且已在发布到应用商店的应用程序中使用)
想要完整的来源吗?
在下载源代码之前,请理解该示例需要进行许多优化。 Vuforia的示例旨在支持iOS 4,因此,如果您以iOS 6及更高版本为目标,则可以省去至少一半的代码,可以将项目转换为ARC(当然建议这样做),也可以优化视频播放以使用硬件加速。 我已经为我发布的应用程序实现了所有这些功能,但是,在此处编写一个可以同时解决许多问题的教程会令人困惑。
在这里获取源代码!