Skip to content

playerRecord

shixuemei edited this page May 23, 2017 · 6 revisions

短视频录制功能也可以理解为录屏功能,可以将当前正在播放的视频(可以包含有UI界面)录制为mp4或flv格式的文件保存到本地。

如果不需要录制UI,可以直接获取播放器返回的原始音视频数据写入文件。本文则重点介绍如何完成含有UI的播放录制功能。

一、依赖组件

因为视频录制功能需要音视频的编码和复用功能,而这些功能对于独立的播放SDK并不存在也不需要,所以要使用短视频录制功能,需要集成的SDK为融合版SDK,使用其中的KSYGPUPicMixerKSYAudioMixerKSYGPUPicOutput等模块

二、方案

2.1 方案一(图层混合方案)

v1.9.5及以上版本支持

播放时通过播放器中的textureBlock获取渲染的TextureId,与UIElement叠加后得到视频数据送入KSYStreamerBase模块;原始音频数据则从播放器的audioDataBlock中获取,经过KSYAudioMixer后送入KSYStreamerBase模块,示意图如下:

播放录屏方案一

优点:

  • 支持自定义录制UI层级
  • 能够按照视频帧率录制

缺点:

  • 在某些设备上及资源占用严重
  • 不支持视频播放过程中的缩放、旋转

2.2 方案二(截屏方案)

v2.1.1及以上版本支持

音频数据的获取与处理方式同方案一,不同之处是视频和UI数据的获取,方案二通过直接截取UIView上内容来获取当前的显示内容,转换为CVPixerBuffer后直接送入KSYStreamerBase模块,示意图如下:

播放录屏方案二

优点:

  • 支持自定义录制UI层级
  • 可自定义录制帧率
  • 资源占用相对较少
  • 可以把视频播放过程中的缩放、旋转原样录下来

缺点:

  • 无法按照视频播放的帧率进行录制

三、使用说明

KSYLive_iOS的Demo中提供了KSYUIRecorderKit类实现录屏功能,KSYRecordVC类中演示了如何使用KSYUIRecorderKit进行短视频录制 录制功能的具体实现请参见KSYUIRecorderKit.m文件源码,此处简单介绍直接使用该类的方法。

3.1 引入所需头文件

#import "KSYUIRecorderKit.h"
#import <GPUImage/GPUImage.h>
#import <libksygpulive/libksygpulive.h>

3.2 声明KSYMoviePlayerController对象和KSYUIRecorderKit对象

@property (strong, nonatomic) KSYMoviePlayerController *player;
@property (strong, nonatomic) KSYUIRecorderKit* kit;

3.3 初始化KSYUIRecorderKit对象

3.3.1 使用方案一时的KSYUIRecorderKit对象初始化
_kit = [[KSYUIRecorderKit alloc]initWithScheme:KSYPlayerRecord_PicMix_Scheme];

_kit = [[KSYUIRecorderKit alloc]init];
3.3.2 使用方案二时的KSYUIRecorderKit对象初始化
_kit = [[KSYUIRecorderKit alloc]initWithScheme:KSYPlayerRecord_ScreenShot_Scheme];

3.4 初始化KSYMoviePlayerController对象

3.4.1 使用方案一时的KSYMoviePlayerController对象初始化

此处对播放器的初始化使用initWithContentURL:sharegroup:进行初始化操作

self.player = [[KSYMoviePlayerController alloc] initWithContentURL:_url sharegroup:[[[GPUImageContext sharedImageProcessingContext] context] sharegroup]];

//player视频数据输入
__weak KSYUIRecorderKit* weakKit = _kit;
_player.textureBlock = ^(GLuint textureId, int width, int height, double pts){
    CGSize size = CGSizeMake(width, height);
    CMTime _pts = CMTimeMake((int64_t)(pts * 1000), 1000);
    [weakKit processWithTextureId:textureId TextureSize:size Time:_pts];
};

//player音频数据输入
_player.audioDataBlock = ^(CMSampleBufferRef buf){
    CMTime pts = CMSampleBufferGetPresentationTimeStamp(buf);
    if(pts.value < 0)
    {            
	return;
    }
    [weakKit processAudioSampleBuffer:buf];
};
3.4.2 使用方案二时的KSYMoviePlayerController对象初始化
self.player = [[KSYMoviePlayerController alloc] initWithContentURL:_url sharegroup:nil];
@WeakObj(_kit);
_player.audioDataBlock = ^(CMSampleBufferRef buf){
    CMTime pts = CMSampleBufferGetPresentationTimeStamp(buf);
    if(pts.value < 0)
    {            
	return;
    }
    [weakKit processAudioSampleBuffer:buf];
};

3.5 显示内容获取

3.5.1 使用方案一时的显示内容获取
//addUIToKit将需要录制的界面元素添加到_kit.contentView,未添加的元素将不能被录制下来
-(void)addUIToKit{	    
        [_kit.contentView addSubview:labelVolume];
	[_kit.contentView addSubview:sliderVolume];
	[_kit.contentView addSubview:lableHWCodec];
	[_kit.contentView addSubview:switchHwCodec];
	[_kit.contentView addSubview:btnPlay];
	[_kit.contentView addSubview:btnPause];
	[_kit.contentView addSubview:btnResume];
	[_kit.contentView addSubview:btnStop];
	[_kit.contentView addSubview:btnQuit];
	[_kit.contentView addSubview:btnStartRecord];
	[_kit.contentView addSubview:btnStopRecord];
	[_kit.contentView addSubview:stat];
	   
	[self.view addSubview:_kit.contentView];
	[_kit.contentView sendSubviewToBack:videoView];
}
3.5.2 使用方案二时的显示内容获取
  • 截取指定UI上的内容
UIImage *screenshot = NULL;
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:NO];
screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
  • UIImage转换为CVPixelBuffer
- (CVPixelBufferRef)pixelBufferFromCGImage:(CGImageRef)image
{
    CVPixelBufferRef pixelBuffer = NULL;
    CGFloat imageWidth = 0;
    CGFloat imageHeight = 0;
    CVReturn status = kCVReturnError;
    
    if(!image)
        return nil;
    
    NSMutableDictionary *options = [[NSMutableDictionary alloc]init];
    [options setObject:[NSNumber numberWithBool:YES] forKey:(NSString *)kCVPixelBufferCGImageCompatibilityKey];
    [options setObject:[NSNumber numberWithBool:YES] forKey:(NSString *)kCVPixelBufferCGBitmapContextCompatibilityKey];
    if(SYSTEM_VERSION_LESS_THAN(@"9.0"))
        [options setObject:@{} forKey:(NSString *)kCVPixelBufferIOSurfacePropertiesKey];
    
    imageWidth = CGImageGetWidth(image);
    imageHeight = CGImageGetHeight(image);
    
    status = CVPixelBufferCreate(kCFAllocatorDefault,
                                 imageWidth,
                                 imageHeight,
                                 kCVPixelFormatType_32BGRA,
                                 (__bridge CFDictionaryRef) options,
                                 &pixelBuffer);
    
    if(status != kCVReturnSuccess || !pixelBuffer)
        return nil;
    
    CVPixelBufferLockBaseAddress(pixelBuffer, 0);
    void *pixelData = CVPixelBufferGetBaseAddress(pixelBuffer);
    
    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    
    CGContextRef context = CGBitmapContextCreate(pixelData,
                                                 imageWidth,
                                                 imageHeight,
                                                 8,
                                                 CVPixelBufferGetBytesPerRow(pixelBuffer),
                                                 rgbColorSpace,
                                                 kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little);
    
    CGContextConcatCTM(context, CGAffineTransformIdentity);
    CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), image);
    
    CGColorSpaceRelease(rgbColorSpace);
    CGContextRelease(context);
    
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
    
    return pixelBuffer;
}

更详细的处理逻辑,请参考KSYRecordVC.m文件

6. 开始录制

path为录制文件的保存路径,目前支持mp4和flv格式,依靠后缀名区分

NSString* recordFilePath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/RecordAv.mp4"];
NSURL * path =[[NSURL alloc] initWithString:recordFilePath];
[_kit startRecord:path];

7. 停止录制

[_kit stopRecord];

四、录制完成后对于文件的操作

因为stopRecord操作为异步操作,所以stopRecord调用结束后并不表示文件已经写完,需要用以下方式处理

  1. 启动录制前注册KSYStreamStateDidChangeNotification消息
[[NSNotificationCenter defaultCenter]addObserver:self
                                   selector:@selector(onStreamStateChange:)
                                   name:(KSYStreamStateDidChangeNotification)
                                   object:nil];
  1. 当收到KSYStreamStateDidChangeNotification消息,且_kit.writer.streamState的值为KSYStreamStateIdle时才表示文件已经写完,此时可以进一步处理文件,否则有可能因为文件被占用导致操作失败
- (void) onStreamStateChange :(NSNotification *)notification{	    
      if (_kit.writer.streamState == KSYStreamStateIdle && _kit.bPlayRecord == NO){
	    [self saveVideoToAlbum: recordFilePath];
      }	    
}
Clone this wiki locally