两周没有更新博客了,MOMO最近超忙各种加班进行中。。IOS + Android同时开发,激情的日子继续着,蛤蛤。昨天有个朋友打电话告诉我说它们的U3D项目遇到点棘手的难题,他们想在Unity3D中增加截屏录像录音的功能,并且还要能导出保存成.mp4的格式。据我所知Unity3D是没有截屏录像的功能,只有截屏图片的功能。有一段时间没有研究Unity3D的东西了,一时心里痒痒我决定那就好好研究研究,功夫不负有心人终于让我研究出来如何在Unity3D结合IOS前端录制截屏视频的功能了。 . 首先我说说我研究实现的原理。1.截取屏幕每帧的图片,将截取的N张图片组成一个没有声音的视频.mp4文件。2.同时还需要录制手机听筒中的声音保存为.caf格式。3.最终将没有声音的视频和音频组合成一个全新的视频文件,保存在沙盒中即可。 . 因为他们的Unity3D项目比较特殊,可以认为是在Unity3D游戏引擎之上搭建的IOS软件项目。Unity3D只负责显示一个3D的模型,至于UI部分全部都是由IOS的前台的OC代码实现的。这样就造成一个问题,OC的代码截图只有UI部分,U3D截图只有3D部分。为了解决这个问题截屏时我们需要把这两张图片合成为一张全新的图片。这里再说一下用苹果私有API截图是可以同时将UI部分U3D部分保存为一张图片,不过有可能APPStore不能审核通过所以大家还是老老实实用合并的方法来做。 . OK下面MOMO来说代码的实现过程
首先在Unity中创建一个全新的工程,在创建一个立方体对象,为了方便看效果我们写一条脚本让这个立方体的对象一直自转,这样播放出来的视频看的会比较清楚喔。 Test.cs直接挂在立方体对象之上。代码比较简单我就不解释了。
1. using UnityEngine;
2. using System.Collections;
3. 4. public class Test : MonoBehaviour {
5. 6. int count = 0;
7. void Start () {
8. 9. }
10. 11. // Update is called once per frame
12. void Update ()
13. {
14. 15. this.transform.Rotate(new Vector3(0,1,0));
16. }
17. 18. //在这里OC的代码通知U3D截屏
19. void StartScreenshot(string str)
20. {
21. Application.CaptureScreenshot(count +"u3d.JPG");
22. count++;
23. }
24. }
复制代码
然后我们将这个Unity3D工程导出成IOS的项目 。Unity会生成对应的XCODE工程。我们写一个全新的ViewController覆盖在U3D生成的OPGL viewController之上,用于写UI高级控件,接着打开APPControll.mm文件。在如下方法的末尾处添加代码
1. int OpenEAGL_UnityCallback(UIWindow** window, int* screenWidth, int* screenHeight, int* openglesVersion)
2. {
3. CGRect rect = [[UIScreen mainScreen] bounds];
4. 5. // Create a full-screen window
6. _window = [[UIWindow alloc] initWithFrame:rect];
7. EAGLView* view = [[EAGLView alloc] initWithFrame:rect];
8. UnityViewController *controller = [[UnityViewController alloc] init];
9. 10. sGLViewController = controller;
11. sGLView = view;
12. 13. #if defined(__IPHONE_3_0)
14. if( _ios30orNewer )
15. controller.wantsFullScreenLayout = TRUE;
16. #endif
17. 18. controller.view = view;
19. 20. CreateSplashView( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ? (UIView*)_window : (UIView*)view );
21. CreateActivityIndicator(_splashView);
22. 23. // add only now so controller have chance to reorient *all* added views
24. [_window addSubview:view];
25. if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
26. [_window bringSubviewToFront:_splashView];
27. 28. _autorotEnableHandling = true;
29. [[NSNotificationCenter defaultCenter] postNotificationName: UIDeviceOrientationDidChangeNotification object: [UIDevice currentDevice]];
30. 31. // reposition activity indicator after we rotated views
32. if (_activityIndicator)
33. _activityIndicator.center = CGPointMake([_splashView bounds].size.width/2, [_splashView bounds].size.height/2);
34. 35. int openglesApi =
36. #if defined(__IPHONE_3_0) && USE_OPENGLES20_IF_AVAILABLE
37. kEAGLRenderingAPIOpenGLES2;
38. #else
39. kEAGLRenderingAPIOpenGLES1;
40. #endif
41. 42. for (; openglesApi >= kEAGLRenderingAPIOpenGLES1 && !_context; --openglesApi)
43. {
44. if (!UnityIsRenderingAPISupported(openglesApi))
45. continue;
46. 47. _context = [[EAGLContext alloc] initWithAPI:openglesApi];
48. }
49. 50. if (!_context)
51. return false;
52. 53. if (![EAGLContext setCurrentContext:_context]) {
54. _context = 0;
55. return false;
56. }
57. 58. const GLuint colorFormat = UnityUse32bitDisplayBuffer() ? GL_RGBA8_OES : GL_RGB565_OES;
59. 60. if (!CreateWindowSurface(view, colorFormat, GL_DEPTH_COMPONENT16_OES, UnityGetDesiredMSAASampleCount(MSAA_DEFAULT_SAMPLE_COUNT), NO, &_surface)) {
61. return false;
62. }
63. 64. glViewport(0, 0, _surface.w, _surface.h);
65. [_window makeKeyAndVisible];
66. [view release];
67. 68. *window = _window;
69. *screenWidth = _surface.w;
70. *screenHeight = _surface.h;
71. *openglesVersion = _context.API;
72. 73. _glesContextCreated = true;
74. 75. //--------------------下面的MyViewController就是我们新写的Contoller----------------
76. MyViewController * myView = [[MyViewController alloc] init];
77. [sGLViewController.view addSubview:myView.view];
78. //--------------------上面的MyViewController就是我们新写的Contoller----------------
79. return true;
80. }
复制代码
1. int OpenEAGL_UnityCallback(UIWindow** window, int* screenWidth, int* screenHeight, int* openglesVersion)
2. {
3. CGRect rect = [[UIScreen mainScreen] bounds];
4. 5. // Create a full-screen window
6. _window = [[UIWindow alloc] initWithFrame:rect];
7. EAGLView* view = [[EAGLView alloc] initWithFrame:rect];
8. UnityViewController *controller = [[UnityViewController alloc] init];
9. 10. sGLViewController = controller;
11. sGLView = view;
12. 13. #if defined(__IPHONE_3_0)
14. if( _ios30orNewer )
15. controller.wantsFullScreenLayout = TRUE;
16. #endif
17. 18. controller.view = view;
19. 20. CreateSplashView( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ? (UIView*)_window : (UIView*)view );
21. CreateActivityIndicator(_splashView);
22. 23. // add only now so controller have chance to reorient *all* added views
24. [_window addSubview:view];
25. if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
26. [_window bringSubviewToFront:_splashView];
27. 28. _autorotEnableHandling = true;
29. [[NSNotificationCenter defaultCenter] postNotificationName: UIDeviceOrientationDidChangeNotification object: [UIDevice currentDevice]];
30. 31. // reposition activity indicator after we rotated views
32. if (_activityIndicator)
33. _activityIndicator.center = CGPointMake([_splashView bounds].size.width/2, [_splashView bounds].size.height/2);
34. 35. int openglesApi =
36. #if defined(__IPHONE_3_0) && USE_OPENGLES20_IF_AVAILABLE
37. kEAGLRenderingAPIOpenGLES2;
38. #else
39. kEAGLRenderingAPIOpenGLES1;
40. #endif
41. 42. for (; openglesApi >= kEAGLRenderingAPIOpenGLES1 && !_context; --openglesApi)
43. {
44. if (!UnityIsRenderingAPISupported(openglesApi))
45. continue;
46. 47. _context = [[EAGLContext alloc] initWithAPI:openglesApi];
48. }
49. 50. if (!_context)
51. return false;
52. 53. if (![EAGLContext setCurrentContext:_context]) {
54. _context = 0;
55. return false;
56. }
57. 58. const GLuint colorFormat = UnityUse32bitDisplayBuffer() ? GL_RGBA8_OES : GL_RGB565_OES;
59. 60. if (!CreateWindowSurface(view, colorFormat, GL_DEPTH_COMPONENT16_OES, UnityGetDesiredMSAASampleCount(MSAA_DEFAULT_SAMPLE_COUNT), NO, &_surface)) {
61. return false;
62. }
63. 64. glViewport(0, 0, _surface.w, _surface.h);
65. [_window makeKeyAndVisible];
66. [view release];
67. 68. *window = _window;
69. *screenWidth = _surface.w;
70. *screenHeight = _surface.h;
71. *openglesVersion = _context.API;
72. 73. _glesContextCreated = true;
74. 75. //--------------------下面的MyViewController就是我们新写的Contoller----------------
76. MyViewController * myView = [[MyViewController alloc] init];
77. [sGLViewController.view addSubview:myView.view];
78. //--------------------上面的MyViewController就是我们新写的Contoller----------------
79. return true;
80. }
复制代码
如果你不是U3D项目请大家记得引入AVFoundation.formwork 和 MediaPlayer.framework ,因为U3D会自动将这两个fromWork生成出来
1. //
2. // MyViewController.h
3. // avcount
4. //
5. // Created by 雨松MOMO on 12-9-14.
6. // Copyright (c) 2012年 雨松MOMO. All rights reserved.
7. //
8. 9. #import
10. #import
11. #import
12. #import
13. 14. @interface MyViewController : UIViewController
15. {
16. //时间计时器
17. NSTimer *_timer;
18. int _count;
19. UILabel * _labe;
20. //录音
21. AVAudioRecorder * _recorder;
22. //读取动画
23. UITextView *_sharedLoadingTextView;
24. UIActivityIndicatorView* _sharedActivityView;
25. 26. }
27. 28. @end
复制代码
下面是具体的实现 ,核心的代码MOMO也是在网上学习老外的文章,最终将它们组合在了一起就完成了。研究了好几个小时,真实内牛满面啊~~~~ 由于代码比较多,请大家一定要仔细阅读哦。。
1. //
2. // MyViewController.m
3. // avcount
4. //
5. // Created by 雨松MOMO on 12-9-14.
6. // Copyright (c) 2012年 雨松MOMO. All rights reserved.
7. //
8. 9. #import "MyViewController.h"
10. 11. @interface MyViewController ()
12. 13. @end
14. 15. @implementation MyViewController
16. 17. - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
18. {
19. self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
20. if (self) {
21. // Custom initialization
22. }
23. return self;
24. }
25. 26. - (void)viewDidLoad
27. {
28. [super viewDidLoad];
29. self.view.backgroundColor = [UIColor redColor];
30. UIWindow *screenWindow = [[UIApplication sharedApplication] keyWindow];
31. UIGraphicsBeginImageContext(screenWindow.frame.size);
32. [screenWindow.layer renderInContext:UIGraphicsGetCurrentContext()];
33. UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
34. UIGraphicsEndImageContext();
35. UIImageWriteToSavedPhotosAlbum(viewImage, nil, nil, nil);
36. 37. #if !TARGET_IPHONE_SIMULATOR
38. self.view.backgroundColor = [UIColor greenColor];
39. #else
40. self.view.backgroundColor = [UIColor clearColor];
41. #endif
42. 43. UIButton * start = [UIButton buttonWithType:UIButtonTypeRoundedRect];
44. [start setFrame:CGRectMake(0, 0, 200, 30)];
45. [start setTitle:@"开始截屏" forState:UIControlStateNormal];
46. [start addTarget:self action:@selector(startPress) forControlEvents:UIControlEventTouchDown];
47. 48. UIButton * end = [UIButton buttonWithType:UIButtonTypeRoundedRect];
49. [end setFrame:CGRectMake(0, 50, 200, 30)];
50. [end setTitle:@"结束截屏(开始录制视频)" forState:UIControlStateNormal];
51. [end addTarget:self action:@selector(endPress) forControlEvents:UIControlEventTouchDown];
52. 53. [self.view addSubview:start];
54. [self.view addSubview:end];
55. 56. _labe = [[[UILabel alloc]initWithFrame:CGRectMake(30, 200, 300, 30)]autorelease];
57. _labe.text = [NSString stringWithFormat:@"%@%d",@"雨松MOMO开始计时:=== ",_count];
58. [self.view addSubview:_labe];
59. 60. //初始化录音
61. [self prepareToRecord];
62. }
63. 64. -(void)addLoading:(NSString*) info
65. {
66. //顶部文本视图
67. _sharedLoadingTextView = [[[UITextView alloc] initWithFrame:CGRectMake(0, 0, 130, 130)] autorelease];
68. [_sharedLoadingTextView setBackgroundColor:[UIColor blackColor]];
69. [_sharedLoadingTextView setText:info];
70. [_sharedLoadingTextView setTextColor:[UIColor whiteColor]];
71. [_sharedLoadingTextView setTextAlignment:UITextAlignmentCenter];
72. [_sharedLoadingTextView setFont:[UIFont systemFontOfSize:15]];
73. _sharedLoadingTextView.textAlignment = UITextAlignmentCenter;
74. _sharedLoadingTextView.alpha = 0.8f;
75. _sharedLoadingTextView.center = self.view.center;
76. _sharedLoadingTextView.layer.cornerRadius = 10;
77. _sharedLoadingTextView.layer.masksToBounds = YES;
78. 79. //创建Loading动画视图
80. _sharedActivityView = [[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray] autorelease];
81. //设置动画视图的风格,这里设定它为白色
82. _sharedActivityView.activityIndicatorViewStyle=UIActivityIndicatorViewStyleWhiteLarge;
83. //设置它显示的区域
84. _sharedActivityView.frame = CGRectMake(0,0, 320, 480);
85. 86. _sharedActivityView.center = self.view.center;
87. //开始播放动画
88. [_sharedActivityView startAnimating];
89. 90. [self.view addSubview:_sharedLoadingTextView];
91. [self.view addSubview:_sharedActivityView];
92. 93. }
94. 95. -(void)removeLoading
96. {
97. [_sharedLoadingTextView removeFromSuperview];
98. [_sharedActivityView removeFromSuperview];
99. }
2.
3. -(void)startPress
4. {
5. 6. _count = 0;
7. _timer = [NSTimer scheduledTimerWithTimeInterval: 0.1
8. target: self
9. selector: @selector(heartBeat:)
10. userInfo: nil
11. repeats: YES];
12. 13. //开始录音
14. [_recorder record];
15. }
16. 17. -(void)endPress
18. {
19. 20. if(_timer != nil)
21. {
22. [_timer invalidate];
23. _timer = nil;
24. 25. [self addLoading:@"开始制作视频"];
26. [NSThread detachNewThreadSelector:@selector(startThreadMainMethod) toTarget:self withObject:nil];
27. }
28. }
29. 30. -(void)startThreadMainMethod
31. {
32. //在这里制作视频
33. NSMutableArray *_array = [[[NSMutableArray alloc]init]autorelease];
34. NSString * Path = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
35. for(int i =0; i< _count; i++)
36. {
37. 38. //读取存在沙盒里面的文件图片
39. NSString * _pathSecond = [NSString stringWithFormat:@"%@/%d%@",Path,i,@".JPG"];
40. NSString * _pathFirst = [NSString stringWithFormat:@"%@/%d%@",Path,i,@"u3d.JPG"];
41. 42. //因为拿到的是个路径 把它加载成一个data对象
43. NSData *data0=[NSData dataWithContentsOfFile:_pathFirst];
44. NSData *data1=[NSData dataWithContentsOfFile:_pathSecond];
45. //直接把该 图片读出来
46. UIImage *img0=[UIImage imageWithData:data0];
47. UIImage *img1=[UIImage imageWithData:data1];
48. 49. [_array addObject:[self MergerImage : img0 : img1]];
50. }
51. 52. Path = [NSString stringWithFormat:@"%@/%@%@",Path,@"veido",@".MP4"];
53. 54. [_recorder stop];
55. [self writeImages:_array ToMovieAtPath:Path withSize: CGSizeMake(320, 480) inDuration:_count*0.1 byFPS:10];
56. [self removeLoading];
57. 58. NSLog(@"recorder successfully");
59. UIAlertView *recorderSuccessful = [[UIAlertView alloc] initWithTitle:@"" message:@"视频录制成功"
60. delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
61. [recorderSuccessful show];
62. [recorderSuccessful release];
63. 64. }
65. 66. - (void) heartBeat: (NSTimer*) timer
67. {
68. _labe.text = [NSString stringWithFormat:@"%@%d",@"雨松MOMO开始计时:=== ",_count];
69. 70. //这个是私有API运气不好会被拒接
71. //这个方法比较给力 可以直接把ios前端和 U3D中的所有图像都截取出来
72. //extern CGImageRef UIGetScreenImage();
73. //UIImage *image = [UIImage imageWithCGImage:UIGetScreenImage()];
74. //UIImageWriteToSavedPhotosAlbum(image,nil,nil,nil);
75. 76. //保险起见还是用如下方法截图
77. //这个方法不能截取U3D的图像
78. UIWindow *screenWindow = [[UIApplication sharedApplication]keyWindow];
79. UIGraphicsBeginImageContext(screenWindow.frame.size);
80. [screenWindow.layer renderInContext:UIGraphicsGetCurrentContext()];
81. UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
82. UIGraphicsEndImageContext();
83. 84. NSData *data;
85. 86. if (UIImagePNGRepresentation(image) == nil)
87. {
88. data = UIImageJPEGRepresentation(image, 1);
89. }
90. else
91. {
92. data = UIImagePNGRepresentation(image);
93. }
94. 95. NSFileManager *fileManager = [NSFileManager defaultManager];
96. 97. NSString * Path = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
98. 99. [fileManager createDirectoryAtPath:Path withIntermediateDirectories:YES attributes:nil error:nil];
100. Path = [NSString stringWithFormat:@"%@/%d%@",Path,_count,@".JPG"];
101. [fileManager createFileAtPath:Path contents:data attributes:nil];
102. 103. //通知U3D开始截屏
104. UnitySendMessage("Cube","StartScreenshot","");
105. 106. _count++;
107. }
108. 109. //合并图片,把ios前景图片和U3D图片合并在一起
110. -(UIImage*) MergerImage:(UIImage*) firstImg:(UIImage*) secondImg
111. {
112. 113. UIGraphicsBeginImageContext(CGSizeMake(320, 480));
114. 115. [firstImg drawInRect:CGRectMake(0, 0, firstImg.size.width, firstImg.size.height)];
116. 117. [secondImg drawInRect:CGRectMake(0, 0, secondImg.size.width, secondImg.size.height)];
118. 119. UIImage *resultImage=UIGraphicsGetImageFromCurrentImageContext();
120. 121. UIGraphicsEndImageContext();
122. 123. return resultImage;
124. 125. }
126. 127. - (void)viewDidUnload
128. {
129. [super viewDidUnload];
130. // Release any retained subviews of the main view.
131. }
132. 133. - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
134. {
135. return (interfaceOrientation == UIInterfaceOrientationPortrait);
136. }
137. 138. - (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image andSize:(CGSize) size
139. {
140. NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
141. [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
142. [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
143. nil];
144. CVPixelBufferRef pxbuffer = NULL;
145. 146. CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, size.width,
147. size.height, kCVPixelFormatType_32ARGB, (CFDictionaryRef) options,
148. &pxbuffer);
149. NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
150. 151. CVPixelBufferLockBaseAddress(pxbuffer, 0);
152. void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
153. NSParameterAssert(pxdata != NULL);
154. 155. CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
156. CGContextRef context = CGBitmapContextCreate(pxdata, size.width,
157. size.height, 8, 4*size.width, rgbColorSpace,
158. kCGImageAlphaNoneSkipFirst);
159. NSParameterAssert(context);
160. CGContextConcatCTM(context, CGAffineTransformMakeRotation(0));
161. CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image),
162. CGImageGetHeight(image)), image);
163. CGColorSpaceRelease(rgbColorSpace);
164. CGContextRelease(context);
165. 166. CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
167. 168. return pxbuffer;
169. }
170. 171. - (void) writeImages:(NSArray *)imagesArray ToMovieAtPath:(NSString *) path withSize:(CGSize) size
172. inDuration:(float)duration byFPS:(int32_t)fps
173. {
174. 175. //在这里将之前截取的图片合并成一个视频
176. //Wire the writer:
177. NSError *error = nil;
178. AVAssetWriter *videoWriter = [[[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:path]
179. fileType:AVFileTypeQuickTimeMovie
180. error:&error] autorelease];
181. NSParameterAssert(videoWriter);
182. 183. NSDictionary *videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:
184. AVVideoCodecH264, AVVideoCodecKey,
185. [NSNumber numberWithInt:size.width], AVVideoWidthKey,
186. [NSNumber numberWithInt:size.height], AVVideoHeightKey,
187. nil];
188. 189. AVAssetWriterInput* videoWriterInput = [[AVAssetWriterInput
190. assetWriterInputWithMediaType:AVMediaTypeVideo
191. outputSettings:videoSettings] retain];
192. 193. AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor
194. assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput
195. sourcePixelBufferAttributes:nil];
196. NSParameterAssert(videoWriterInput);
197. NSParameterAssert([videoWriter canAddInput:videoWriterInput]);
198. [videoWriter addInput:videoWriterInput];
199. 200. //Start a session:
201. [videoWriter startWriting];
202. [videoWriter startSessionAtSourceTime:kCMTimeZero];
203. 204. //Write some samples:
205. CVPixelBufferRef buffer = NULL;
206. 207. int frameCount = 0;
208. 209. int imagesCount = [imagesArray count];
210. float averageTime = duration/imagesCount;
211. int averageFrame = (int)(averageTime * fps);
212. 213. for(UIImage * img in imagesArray)
214. {
215. buffer = [self pixelBufferFromCGImage:[img CGImage] andSize:size];
216. 217. BOOL append_ok = NO;
218. int j = 0;
219. while (!append_ok && j < 30)
220. {
221. if (adaptor.assetWriterInput.readyForMoreMediaData)
222. {
223. printf("appending %d attemp %d", frameCount, j);
224. 225. CMTime frameTime = CMTimeMake(frameCount,(int32_t) fps);
226. float frameSeconds = CMTimeGetSeconds(frameTime);
227. NSLog(@"frameCount:%d,kRecordingFPS:%d,frameSeconds:%f",frameCount,fps,frameSeconds);
228. append_ok = [adaptor appendPixelBuffer:buffer withPresentationTime:frameTime];
229. 230. if(buffer)
231. [NSThread sleepForTimeInterval:0.05];
232. }
233. else
234. {
235. printf("adaptor not ready %d, %d", frameCount, j);
236. [NSThread sleepForTimeInterval:0.1];
237. }
238. j++;
239. }
240. if (!append_ok) {
241. printf("error appending image %d times %d", frameCount, j);
242. }
243. 244. frameCount = frameCount + averageFrame;
245. }
246. 247. //Finish the session:
248. [videoWriterInput markAsFinished];
249. [videoWriter finishWriting];
250. NSLog(@"finishWriting");
251. 252. //将静态视频 和声音合并成一个新视频
253. [self CompileFilesToMakeMovie];
254. 255. }
256. 257. - (void) prepareToRecord
258. 259. {
260. 261. AVAudioSession *audioSession = [AVAudioSession sharedInstance];
262. 263. NSError *err = nil;
264. 265. [audioSession setCategory :AVAudioSessionCategoryPlayAndRecord error:&err];
266. 267. if(err){
268. 269. NSLog(@"audioSession: %@ %d %@", [err domain], [err code], [[err userInfo] description]);
270. 271. return;
272. 273. }
274. 275. [audioSession setActive:YES error:&err];
276. 277. err = nil;
278. 279. if(err){
280. 281. NSLog(@"audioSession: %@ %d %@", [err domain], [err code], [[err userInfo] description]);
282. 283. return;
284. 285. }
286. 287. NSMutableDictionary *recordSetting = [[NSMutableDictionary alloc] init];
288. 289. [recordSetting setValue :[NSNumber numberWithInt:kAudioFormatLinearPCM] forKey:AVFormatIDKey];
290. 291. [recordSetting setValue:[NSNumber numberWithFloat:44100.0] forKey:AVSampleRateKey];
292. 293. [recordSetting setValue:[NSNumber numberWithInt: 2] forKey:AVNumberOfChannelsKey];
294. 295. [recordSetting setValue :[NSNumber numberWithInt:16] forKey:AVLinearPCMBitDepthKey];
296. 297. [recordSetting setValue :[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsBigEndianKey];
298. 299. [recordSetting setValue :[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsFloatKey];
300. 301. // Create a new dated file
302. NSString * recorderFilePath = [[NSString stringWithFormat:@"%@/%@.caf", [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"], @"sound"] retain];
303. NSURL *url = [NSURL fileURLWithPath:recorderFilePath];
304. err = nil;
305. _recorder = [[ AVAudioRecorder alloc] initWithURL:url settings:recordSetting error:&err];
306. if(!_recorder){
307. NSLog(@"recorder: %@ %d %@", [err domain], [err code], [[err userInfo] description]);
308. UIAlertView *alert =
309. [[UIAlertView alloc] initWithTitle: @"Warning"
310. message: [err localizedDescription]
311. delegate: nil
312. cancelButtonTitle:@"OK"
313. otherButtonTitles:nil];
314. [alert show];
315. [alert release];
316. return;
317. }
318. //prepare to record
319. [_recorder setDelegate:self];
320. [_recorder prepareToRecord];
321. _recorder.meteringEnabled = YES;
322. BOOL audioHWAvailable = audioSession.inputIsAvailable;
323. if (! audioHWAvailable) {
324. UIAlertView *cantRecordAlert =
325. [[UIAlertView alloc] initWithTitle: @"Warning"
326. message: @"Audio input hardware not available"
327. delegate: nil
328. cancelButtonTitle:@"OK"
329. otherButtonTitles:nil];
330. [cantRecordAlert show];
331. [cantRecordAlert release];
332. return;
333. }
334. }
335. 336. //代理 这里可以监听录音成功
337. - (void)audioRecorderDidFinishRecording:(AVAudioRecorder *) aRecorder successfully:(BOOL)flag
338. {
339. // NSLog(@"recorder successfully");
340. // UIAlertView *recorderSuccessful = [[UIAlertView alloc] initWithTitle:@"" message:@"录音成功"
341. // delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
342. // [recorderSuccessful show];
343. // [recorderSuccessful release];
344. }
345. 346. //代理 这里可以监听录音失败
347. - (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)arecorder error:(NSError *)error
348. {
349. 350. // UIAlertView *recorderFailed = [[UIAlertView alloc] initWithTitle:@"" message:@"发生错误"
351. // delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
352. // [recorderFailed show];
353. // [recorderFailed release];
354. }
355. 356. -(void)CompileFilesToMakeMovie
357. {
358. //这个方法在沙盒中把静态图片组成的视频 与录制的声音合并成一个新视频
359. AVMutableComposition* mixComposition = [AVMutableComposition composition];
360. 361. NSString* audio_inputFileName = @"sound.caf";
362. NSString* audio_inputFilePath = [NSString stringWithFormat:@"%@/%@", [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"], audio_inputFileName] ;
363. NSURL* audio_inputFileUrl = [NSURL fileURLWithPath:audio_inputFilePath];
364. 365. NSString* video_inputFileName = @"veido.mp4";
366. NSString* video_inputFilePath = [NSString stringWithFormat:@"%@/%@", [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"], video_inputFileName] ;
367. NSURL* video_inputFileUrl = [NSURL fileURLWithPath:video_inputFilePath];
368. 369. NSString* outputFileName = @"outputVeido.mov";
370. NSString* outputFilePath = [NSString stringWithFormat:@"%@/%@", [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"], outputFileName] ;
371. NSURL* outputFileUrl = [NSURL fileURLWithPath:outputFilePath];
372. 373. if ([[NSFileManager defaultManager] fileExistsAtPath:outputFilePath])
374. [[NSFileManager defaultManager] removeItemAtPath:outputFilePath error:nil];
375. 376. CMTime nextClipStartTime = kCMTimeZero;
377. 378. AVURLAsset* videoAsset = [[AVURLAsset alloc]initWithURL:video_inputFileUrl options:nil];
379. CMTimeRange video_timeRange = CMTimeRangeMake(kCMTimeZero,videoAsset.duration);
380. AVMutableCompositionTrack *a_compositionVideoTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
381. [a_compositionVideoTrack insertTimeRange:video_timeRange ofTrack:[[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] atTime:nextClipStartTime error:nil];
382. 383. AVURLAsset* audioAsset = [[AVURLAsset alloc]initWithURL:audio_inputFileUrl options:nil];
384. CMTimeRange audio_timeRange = CMTimeRangeMake(kCMTimeZero, audioAsset.duration);
385. AVMutableCompositionTrack *b_compositionAudioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
386. [b_compositionAudioTrack insertTimeRange:audio_timeRange ofTrack:[[audioAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:nextClipStartTime error:nil];
387. 388. AVAssetExportSession* _assetExport = [[AVAssetExportSession alloc] initWithAsset:mixComposition presetName:AVAssetExportPresetHighestQuality];
389. _assetExport.outputFileType = @"com.apple.quicktime-movie";
390. _assetExport.outputURL = outputFileUrl;
391. 392. [_assetExport exportAsynchronouslyWithCompletionHandler:
393. ^(void ) {
394. }
395. ];
396. 397. }
398. 399. @end
复制代码
如下图所示,高级控件的按钮属于UI部分,后面的立方体对象是U3D生成,并且立方体对象在一直的旋转。点击开始截屏按钮时OC 部分 和U3D会每一帧同时截屏,并且此时开始录音。点击结束截屏按钮时,程序先将OC和U3D截屏的图片每一帧两两的组合成一个新图片,然后生成没有声音的视频。 最后将没有声音的视频 和刚刚录制的音频组合成一个全新的视频存在沙盒中即可。
此时我们看一下模拟器中的沙盒文件,”数字”.JPG就是OC截取的图片, “数字+U3D”.JPG就是U3D中截取的图片。Sound.caf就是录制的音频文件,veido.mp4 就是将连续的图片组合成的无声音视频文件,最后的outputVeido.mov就是将无声音的视频文件与音频文件组合成的新视频文件。
双击打开outputVeido.mov视频文件,我们可以直接在QuickTimePlayer中播放它,怎么样功能很给力吧,哈哈哈。U3D也能轻松的实现截屏功能,哈哈哈~~
最后我在说一下,这个代码同时也适用于IOS普通的软件项目中,U3D只是多做了一步合成图片的功能,所有代码都写在MyViewController中请大家仔细看喔。 这两天MOMO还会抽时间研究一下在Android 下如何截屏录制视频,请大家拭目以待噢,哇咔咔。
因为U3D生成的工程比较大,所以我就不上传U3D生成的XCODE代码了,我给出一个纯OC代码的下载地址,最后雨松MOMO祝大家学习愉快。下载地址:http://vdisk.weibo.com/s/cvQOT |