视频直播APP源码开发iOS音频播放流程

概览

随着移动互联网的发展,如今的手机早已不是打电话、发短信那么简单了,播放音乐、视频、录音、拍照等都是很常用的功能。在iOS中对于多媒体的支持是非常强大的,无论是音视频播放、录制,还是对麦克风、摄像头的操作都提供了多套API。在今天的文章中将会对这些内容进行一一介绍:

  1. 音频
    1. 音效
    2. 音乐
    3. 音频会话
    4. 录音
    5. 音频队列服务
  2. 视频
    1. MPMoviePlayerController
    2. MPMoviePlayerViewController
    3. AVPlayer
  3. 摄像头
    1. UIImagePickerController拍照和视频录制
    2. AVFoundation拍照和录制视频
  4. 总结

音频

在iOS中音频播放从形式上可以分为音效播放和音乐播放。前者主要指的是一些短音频播放,通常作为点缀音频,对于这类音频不需要进行进度、循环等控制。后者指的是一些较长的音频,通常是主音频,对于这些音频的播放通常需要进行精确的控制。在iOS中播放两类音频分别使用AudioToolbox.framework和AVFoundation.framework来完成音效和音乐播放。

音效

AudioToolbox.framework是一套基于C语言的框架,使用它来播放音效其本质是将短音频注册到系统声音服务(System Sound Service)。System Sound Service是一种简单、底层的声音播放服务,但是它本身也存在着一些限制:

  • 音频播放时间不能超过30s
  • 数据必须是PCM或者IMA4格式
  • 音频文件必须打包成.caf、.aif、.wav中的一种(注意这是官方文档的说法,实际测试发现一些.mp3也可以播放)

使用System Sound Service 播放音效的步骤如下:

  1. 调用AudioServicesCreateSystemSoundID(   CFURLRef  inFileURL, SystemSoundID*   outSystemSoundID)函数获得系统声音ID。
  2. 如果需要监听播放完成操作,则使用AudioServicesAddSystemSoundCompletion(  SystemSoundID inSystemSoundID,
    CFRunLoopRef  inRunLoop, CFStringRef  inRunLoopMode, AudioServicesSystemSoundCompletionProc  inCompletionRoutine, void*  inClientData)
    方法注册回调函数。
  3. 调用AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID) 或者AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(后者带有震动效果)。

下面是一个简单的示例程序:

//
//  KCMainViewController.m
//  Audio
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  音效播放#import "KCMainViewController.h"
#import <AudioToolbox/AudioToolbox.h>@interface KCMainViewController ()@end@implementation KCMainViewController- (void)viewDidLoad {[super viewDidLoad];[self playSoundEffect:@"videoRing.caf"];
}/***  播放完成回调函数**  @param soundID    系统声音ID*  @param clientData 回调时传递的数据*/
void soundCompleteCallback(SystemSoundID soundID,void * clientData){NSLog(@"播放完成...");
}/***  播放音效文件**  @param name 音频文件名称*/
-(void)playSoundEffect:(NSString *)name{NSString *audioFile=[[NSBundle mainBundle] pathForResource:name ofType:nil];NSURL *fileUrl=[NSURL fileURLWithPath:audioFile];//1.获得系统声音IDSystemSoundID soundID=0;/*** inFileUrl:音频文件url* outSystemSoundID:声音id(此函数会将音效文件加入到系统音频服务中并返回一个长整形ID)*/AudioServicesCreateSystemSoundID((__bridge CFURLRef)(fileUrl), &soundID);//如果需要在播放完之后执行某些操作,可以调用如下方法注册一个播放完成回调函数AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, soundCompleteCallback, NULL);//2.播放音频AudioServicesPlaySystemSound(soundID);//播放音效
//    AudioServicesPlayAlertSound(soundID);//播放音效并震动
}@end

音乐

如果播放较大的音频或者要对音频有精确的控制则System Sound Service可能就很难满足实际需求了,通常这种情况会选择使用AVFoundation.framework中的AVAudioPlayer来实现。AVAudioPlayer可以看成一个播放器,它支持多种音频格式,而且能够进行进度、音量、播放速度等控制。首先简单看一下AVAudioPlayer常用的属性和方法:

属性说明
@property(readonly, getter=isPlaying) BOOL playing是否正在播放,只读
@property(readonly) NSUInteger numberOfChannels音频声道数,只读
@property(readonly) NSTimeInterval duration音频时长
@property(readonly) NSURL *url音频文件路径,只读
@property(readonly) NSData *data音频数据,只读
@property float pan立体声平衡,如果为-1.0则完全左声道,如果0.0则左右声道平衡,如果为1.0则完全为右声道
@property float volume音量大小,范围0-1.0
@property BOOL enableRate是否允许改变播放速率
@property float rate播放速率,范围0.5-2.0,如果为1.0则正常播放,如果要修改播放速率则必须设置enableRate为YES
@property NSTimeInterval currentTime当前播放时长
@property(readonly) NSTimeInterval deviceCurrentTime输出设备播放音频的时间,注意如果播放中被暂停此时间也会继续累加
@property NSInteger numberOfLoops循环播放次数,如果为0则不循环,如果小于0则无限循环,大于0则表示循环次数
@property(readonly) NSDictionary *settings音频播放设置信息,只读
@property(getter=isMeteringEnabled) BOOL meteringEnabled是否启用音频测量,默认为NO,一旦启用音频测量可以通过updateMeters方法更新测量值
对象方法说明
- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError使用文件URL初始化播放器,注意这个URL不能是HTTP URL,AVAudioPlayer不支持加载网络媒体流,只能播放本地文件
- (instancetype)initWithData:(NSData *)data error:(NSError **)outError使用NSData初始化播放器,注意使用此方法时必须文件格式和文件后缀一致,否则出错,所以相比此方法更推荐使用上述方法或- (instancetype)initWithData:(NSData *)data fileTypeHint:(NSString *)utiString error:(NSError **)outError方法进行初始化
- (BOOL)prepareToPlay;加载音频文件到缓冲区,注意即使在播放之前音频文件没有加载到缓冲区程序也会隐式调用此方法。
- (BOOL)play;播放音频文件
- (BOOL)playAtTime:(NSTimeInterval)time在指定的时间开始播放音频
- (void)pause;暂停播放
- (void)stop;停止播放
- (void)updateMeters更新音频测量值,注意如果要更新音频测量值必须设置meteringEnabled为YES,通过音频测量值可以即时获得音频分贝等信息
- (float)peakPowerForChannel:(NSUInteger)channelNumber;获得指定声道的分贝峰值,注意如果要获得分贝峰值必须在此之前调用updateMeters方法
- (float)averagePowerForChannel:(NSUInteger)channelNumber获得指定声道的分贝平均值,注意如果要获得分贝平均值必须在此之前调用updateMeters方法
@property(nonatomic, copy) NSArray *channelAssignments获得或设置播放声道
代理方法说明
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag音频播放完成
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error音频解码发生错误

AVAudioPlayer的使用比较简单:

  1. 初始化AVAudioPlayer对象,此时通常指定本地文件路径。
  2. 设置播放器属性,例如重复次数、音量大小等。
  3. 调用play方法播放。

下面就使用AVAudioPlayer实现一个简单播放器,在这个播放器中实现了播放、暂停、显示播放进度功能,当然例如调节音量、设置循环模式、甚至是声波图像(通过分析音频分贝值)等功能都可以实现,这里就不再一一演示。界面效果如下:

AudioPlayerScreen

当然由于AVAudioPlayer一次只能播放一个音频文件,所有上一曲、下一曲其实可以通过创建多个播放器对象来完成,这里暂不实现。播放进度的实现主要依靠一个定时器实时计算当前播放时长和音频总时长的比例,另外为了演示委托方法,下面的代码中也实现了播放完成委托方法,通常如果有下一曲功能的话播放完可以触发下一曲音乐播放。下面是主要代码:

//
//  ViewController.m
//  KCAVAudioPlayer
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#define kMusicFile @"刘若英 - 原来你也在这里.mp3"
#define kMusicSinger @"刘若英"
#define kMusicTitle @"原来你也在这里"@interface ViewController ()<AVAudioPlayerDelegate>@property (nonatomic,strong) AVAudioPlayer *audioPlayer;//播放器
@property (weak, nonatomic) IBOutlet UILabel *controlPanel; //控制面板
@property (weak, nonatomic) IBOutlet UIProgressView *playProgress;//播放进度
@property (weak, nonatomic) IBOutlet UILabel *musicSinger; //演唱者
@property (weak, nonatomic) IBOutlet UIButton *playOrPause; //播放/暂停按钮(如果tag为0认为是暂停状态,1是播放状态)@property (weak ,nonatomic) NSTimer *timer;//进度更新定时器@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];[self setupUI];}/***  初始化UI*/
-(void)setupUI{self.title=kMusicTitle;self.musicSinger.text=kMusicSinger;
}-(NSTimer *)timer{if (!_timer) {_timer=[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateProgress) userInfo:nil repeats:true];}return _timer;
}/***  创建播放器**  @return 音频播放器*/
-(AVAudioPlayer *)audioPlayer{if (!_audioPlayer) {NSString *urlStr=[[NSBundle mainBundle]pathForResource:kMusicFile ofType:nil];NSURL *url=[NSURL fileURLWithPath:urlStr];NSError *error=nil;//初始化播放器,注意这里的Url参数只能时文件路径,不支持HTTP Url_audioPlayer=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];//设置播放器属性_audioPlayer.numberOfLoops=0;//设置为0不循环_audioPlayer.delegate=self;[_audioPlayer prepareToPlay];//加载音频文件到缓存if(error){NSLog(@"初始化播放器过程发生错误,错误信息:%@",error.localizedDescription);return nil;}}return _audioPlayer;
}/***  播放音频*/
-(void)play{if (![self.audioPlayer isPlaying]) {[self.audioPlayer play];self.timer.fireDate=[NSDate distantPast];//恢复定时器}
}/***  暂停播放*/
-(void)pause{if ([self.audioPlayer isPlaying]) {[self.audioPlayer pause];self.timer.fireDate=[NSDate distantFuture];//暂停定时器,注意不能调用invalidate方法,此方法会取消,之后无法恢复}
}/***  点击播放/暂停按钮**  @param sender 播放/暂停按钮*/
- (IBAction)playClick:(UIButton *)sender {if(sender.tag){sender.tag=0;[sender setImage:[UIImage imageNamed:@"playing_btn_play_n"] forState:UIControlStateNormal];[sender setImage:[UIImage imageNamed:@"playing_btn_play_h"] forState:UIControlStateHighlighted];[self pause];}else{sender.tag=1;[sender setImage:[UIImage imageNamed:@"playing_btn_pause_n"] forState:UIControlStateNormal];[sender setImage:[UIImage imageNamed:@"playing_btn_pause_h"] forState:UIControlStateHighlighted];[self play];}
}/***  更新播放进度*/
-(void)updateProgress{float progress= self.audioPlayer.currentTime /self.audioPlayer.duration;[self.playProgress setProgress:progress animated:true];
}#pragma mark - 播放器代理方法
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{NSLog(@"音乐播放完成...");
}@end

运行效果:

AVAudioPlayer

音频会话

事实上上面的播放器还存在一些问题,例如通常我们看到的播放器即使退出到后台也是可以播放的,而这个播放器如果退出到后台它会自动暂停。如果要支持后台播放需要做下面几件事情:

1.设置后台运行模式:在plist文件中添加Required background modes,并且设置item 0=App plays audio or streams audio/video using AirPlay(其实可以直接通过Xcode在Project Targets-Capabilities-Background Modes中设置)

BackgroundModes

2.设置AVAudioSession的类型为AVAudioSessionCategoryPlayback并且调用setActive::方法启动会话。

AVAudioSession *audioSession=[AVAudioSession sharedInstance];[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];[audioSession setActive:YES error:nil];

3.为了能够让应用退到后台之后支持耳机控制,建议添加远程控制事件(这一步不是后台播放必须的)

前两步是后台播放所必须设置的,第三步主要用于接收远程事件,这部分内容之前的文章中有详细介绍,如果这一步不设置虽让也能够在后台播放,但是无法获得音频控制权(如果在使用当前应用之前使用其他播放器播放音乐的话,此时如果按耳机播放键或者控制中心的播放按钮则会播放前一个应用的音频),并且不能使用耳机进行音频控制。第一步操作相信大家都很容易理解,如果应用程序要允许运行到后台必须设置,正常情况下应用如果进入后台会被挂起,通过该设置可以上应用程序继续在后台运行。但是第二步使用的AVAudioSession有必要进行一下详细的说明。

在iOS中每个应用都有一个音频会话,这个会话就通过AVAudioSession来表示。AVAudioSession同样存在于AVFoundation框架中,它是单例模式设计,通过sharedInstance进行访问。在使用Apple设备时大家会发现有些应用只要打开其他音频播放就会终止,而有些应用却可以和其他应用同时播放,在多种音频环境中如何去控制播放的方式就是通过音频会话来完成的。下面是音频会话的几种会话模式:

会话类型说明是否要求输入是否要求输出是否遵从静音键
AVAudioSessionCategoryAmbient混音播放,可以与其他音频应用同时播放
AVAudioSessionCategorySoloAmbient独占播放
AVAudioSessionCategoryPlayback后台播放,也是独占的
AVAudioSessionCategoryRecord录音模式,用于录音时使用
AVAudioSessionCategoryPlayAndRecord播放和录音,此时可以录音也可以播放
AVAudioSessionCategoryAudioProcessing硬件解码音频,此时不能播放和录制
AVAudioSessionCategoryMultiRoute多种输入输出,例如可以耳机、USB设备同时播放

注意:是否遵循静音键表示在播放过程中如果用户通过硬件设置为静音是否能关闭声音。

根据前面对音频会话的理解,相信大家开发出能够在后台播放的音频播放器并不难,但是注意一下,在前面的代码中也提到设置完音频会话类型之后需要调用setActive::方法将会话激活才能起作用。类似的,如果一个应用已经在播放音频,打开我们的应用之后设置了在后台播放的会话类型,此时其他应用的音频会停止而播放我们的音频,如果希望我们的程序音频播放完之后(关闭或退出到后台之后)能够继续播放其他应用的音频的话则可以调用setActive::方法关闭会话。代码如下:

//
//  ViewController.m
//  KCAVAudioPlayer
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  AVAudioSession 音频会话#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#define kMusicFile @"刘若英 - 原来你也在这里.mp3"
#define kMusicSinger @"刘若英"
#define kMusicTitle @"原来你也在这里"@interface ViewController ()<AVAudioPlayerDelegate>@property (nonatomic,strong) AVAudioPlayer *audioPlayer;//播放器
@property (weak, nonatomic) IBOutlet UILabel *controlPanel; //控制面板
@property (weak, nonatomic) IBOutlet UIProgressView *playProgress;//播放进度
@property (weak, nonatomic) IBOutlet UILabel *musicSinger; //演唱者
@property (weak, nonatomic) IBOutlet UIButton *playOrPause; //播放/暂停按钮(如果tag为0认为是暂停状态,1是播放状态)@property (weak ,nonatomic) NSTimer *timer;//进度更新定时器@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];[self setupUI];}/***  显示当面视图控制器时注册远程事件**  @param animated 是否以动画的形式显示*/
-(void)viewWillAppear:(BOOL)animated{[super viewWillAppear:animated];//开启远程控制[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];//作为第一响应者//[self becomeFirstResponder];
}
/***  当前控制器视图不显示时取消远程控制**  @param animated 是否以动画的形式消失*/
-(void)viewWillDisappear:(BOOL)animated{[super viewWillDisappear:animated];[[UIApplication sharedApplication] endReceivingRemoteControlEvents];//[self resignFirstResponder];
}/***  初始化UI*/
-(void)setupUI{self.title=kMusicTitle;self.musicSinger.text=kMusicSinger;
}-(NSTimer *)timer{if (!_timer) {_timer=[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateProgress) userInfo:nil repeats:true];}return _timer;
}/***  创建播放器**  @return 音频播放器*/
-(AVAudioPlayer *)audioPlayer{if (!_audioPlayer) {NSString *urlStr=[[NSBundle mainBundle]pathForResource:kMusicFile ofType:nil];NSURL *url=[NSURL fileURLWithPath:urlStr];NSError *error=nil;//初始化播放器,注意这里的Url参数只能时文件路径,不支持HTTP Url_audioPlayer=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];//设置播放器属性_audioPlayer.numberOfLoops=0;//设置为0不循环_audioPlayer.delegate=self;[_audioPlayer prepareToPlay];//加载音频文件到缓存if(error){NSLog(@"初始化播放器过程发生错误,错误信息:%@",error.localizedDescription);return nil;}//设置后台播放模式AVAudioSession *audioSession=[AVAudioSession sharedInstance];[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
//        [audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil];[audioSession setActive:YES error:nil];//添加通知,拔出耳机后暂停播放[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];}return _audioPlayer;
}/***  播放音频*/
-(void)play{if (![self.audioPlayer isPlaying]) {[self.audioPlayer play];self.timer.fireDate=[NSDate distantPast];//恢复定时器}
}/***  暂停播放*/
-(void)pause{if ([self.audioPlayer isPlaying]) {[self.audioPlayer pause];self.timer.fireDate=[NSDate distantFuture];//暂停定时器,注意不能调用invalidate方法,此方法会取消,之后无法恢复}
}/***  点击播放/暂停按钮**  @param sender 播放/暂停按钮*/
- (IBAction)playClick:(UIButton *)sender {if(sender.tag){sender.tag=0;[sender setImage:[UIImage imageNamed:@"playing_btn_play_n"] forState:UIControlStateNormal];[sender setImage:[UIImage imageNamed:@"playing_btn_play_h"] forState:UIControlStateHighlighted];[self pause];}else{sender.tag=1;[sender setImage:[UIImage imageNamed:@"playing_btn_pause_n"] forState:UIControlStateNormal];[sender setImage:[UIImage imageNamed:@"playing_btn_pause_h"] forState:UIControlStateHighlighted];[self play];}
}/***  更新播放进度*/
-(void)updateProgress{float progress= self.audioPlayer.currentTime /self.audioPlayer.duration;[self.playProgress setProgress:progress animated:true];
}/***  一旦输出改变则执行此方法**  @param notification 输出改变通知对象*/
-(void)routeChange:(NSNotification *)notification{NSDictionary *dic=notification.userInfo;int changeReason= [dic[AVAudioSessionRouteChangeReasonKey] intValue];//等于AVAudioSessionRouteChangeReasonOldDeviceUnavailable表示旧输出不可用if (changeReason==AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {AVAudioSessionRouteDescription *routeDescription=dic[AVAudioSessionRouteChangePreviousRouteKey];AVAudioSessionPortDescription *portDescription= [routeDescription.outputs firstObject];//原设备为耳机则暂停if ([portDescription.portType isEqualToString:@"Headphones"]) {[self pause];}}//    [dic enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
//        NSLog(@"%@:%@",key,obj);
//    }];
}-(void)dealloc{[[NSNotificationCenter defaultCenter] removeObserver:self name:AVAudioSessionRouteChangeNotification object:nil];
}#pragma mark - 播放器代理方法
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{NSLog(@"音乐播放完成...");//根据实际情况播放完成可以将会话关闭,其他音频应用继续播放[[AVAudioSession sharedInstance]setActive:NO error:nil];
}@end

在上面的代码中还实现了拔出耳机暂停音乐播放的功能,这也是一个比较常见的功能。在iOS7及以后的版本中可以通过通知获得输出改变的通知,然后拿到通知对象后根据userInfo获得是何种改变类型,进而根据情况对音乐进行暂停操作。

扩展--播放音乐库中的音乐

众所周知音乐是iOS的重要组成播放,无论是iPod、iTouch、iPhone还是iPad都可以在iTunes购买音乐或添加本地音乐到音乐库中同步到你的iOS设备。在MediaPlayer.frameowork中有一个MPMusicPlayerController用于播放音乐库中的音乐。

下面先来看一下MPMusicPlayerController的常用属性和方法:

属性说明
@property (nonatomic, readonly) MPMusicPlaybackState playbackState播放器状态,枚举类型:
MPMusicPlaybackStateStopped:停止播放 MPMusicPlaybackStatePlaying:正在播放
MPMusicPlaybackStatePaused:暂停播放
MPMusicPlaybackStateInterrupted:播放中断
MPMusicPlaybackStateSeekingForward:向前查找
MPMusicPlaybackStateSeekingBackward:向后查找
@property (nonatomic) MPMusicRepeatMode repeatMode重复模式,枚举类型:
MPMusicRepeatModeDefault:默认模式,使用用户的首选项(系统音乐程序设置)
MPMusicRepeatModeNone:不重复
MPMusicRepeatModeOne:单曲循环
MPMusicRepeatModeAll:在当前列表内循环
@property (nonatomic) MPMusicShuffleMode shuffleMode随机播放模式,枚举类型:
MPMusicShuffleModeDefault:默认模式,使用用户首选项(系统音乐程序设置)
MPMusicShuffleModeOff:不随机播放
MPMusicShuffleModeSongs:按歌曲随机播放
MPMusicShuffleModeAlbums:按专辑随机播放
@property (nonatomic, copy) MPMediaItem *nowPlayingItem正在播放的音乐项
@property (nonatomic, readonly) NSUInteger indexOfNowPlayingItem当前正在播放的音乐在播放队列中的索引
@property(nonatomic, readonly) BOOL isPreparedToPlay是否准好播放准备
@property(nonatomic) NSTimeInterval currentPlaybackTime当前已播放时间,单位:秒
@property(nonatomic) float currentPlaybackRate当前播放速度,是一个播放速度倍率,0表示暂停播放,1代表正常速度
类方法说明
+ (MPMusicPlayerController *)applicationMusicPlayer;获取应用播放器,注意此类播放器无法在后台播放
+ (MPMusicPlayerController *)systemMusicPlayer获取系统播放器,支持后台播放
对象方法说明
- (void)setQueueWithQuery:(MPMediaQuery *)query使用媒体队列设置播放源媒体队列
- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection使用媒体项集合设置播放源媒体队列
- (void)skipToNextItem下一曲
- (void)skipToBeginning从起始位置播放
- (void)skipToPreviousItem上一曲
- (void)beginGeneratingPlaybackNotifications开启播放通知,注意不同于其他播放器,MPMusicPlayerController要想获得通知必须首先开启,默认情况无法获得通知
- (void)endGeneratingPlaybackNotifications关闭播放通知
- (void)prepareToPlay做好播放准备(加载音频到缓冲区),在使用play方法播放时如果没有做好准备回自动调用该方法
- (void)play开始播放
- (void)pause暂停播放
- (void)stop停止播放
- (void)beginSeekingForward开始向前查找(快进)
- (void)beginSeekingBackward开始向后查找(快退)
- (void)endSeeking结束查找
通知说明
(注意:要想获得MPMusicPlayerController通知必须首先调用beginGeneratingPlaybackNotifications开启通知)
MPMusicPlayerControllerPlaybackStateDidChangeNotification播放状态改变
MPMusicPlayerControllerNowPlayingItemDidChangeNotification当前播放音频改变
MPMusicPlayerControllerVolumeDidChangeNotification声音大小改变
MPMediaPlaybackIsPreparedToPlayDidChangeNotification准备好播放
  • MPMusicPlayerController有两种播放器:applicationMusicPlayer和systemMusicPlayer,前者在应用退出后音乐播放会自动停止,后者在应用停止后不会退出播放状态。
  • MPMusicPlayerController加载音乐不同于前面的AVAudioPlayer是通过一个文件路径来加载,而是需要一个播放队列。在MPMusicPlayerController中提供了两个方法来加载播放队列:- (void)setQueueWithQuery:(MPMediaQuery *)query- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection,正是由于它的播放音频来源是一个队列,因此MPMusicPlayerController支持上一曲、下一曲等操作。

那么接下来的问题就是如何获取MPMediaQueue或者MPMediaItemCollection?MPMediaQueue对象有一系列的类方法来获得媒体队列:

+ (MPMediaQuery *)albumsQuery;
+ (MPMediaQuery *)artistsQuery;
+ (MPMediaQuery *)songsQuery;
+ (MPMediaQuery *)playlistsQuery;
+ (MPMediaQuery *)podcastsQuery;
+ (MPMediaQuery *)audiobooksQuery;
+ (MPMediaQuery *)compilationsQuery;
+ (MPMediaQuery *)composersQuery;
+ (MPMediaQuery *)genresQuery;

有了这些方法,就可以很容易获到歌曲、播放列表、专辑媒体等媒体队列了,这样就可以通过:- (void)setQueueWithQuery:(MPMediaQuery *)query方法设置音乐来源了又或者得到MPMediaQueue之后创建MPMediaItemCollection,使用- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection设置音乐来源。

有时候可能希望用户自己来选择要播放的音乐,这时可以使用MPMediaPickerController,它是一个视图控制器,类似于UIImagePickerController,选择完播放来源后可以在其代理方法中获得MPMediaItemCollection对象。

无论是通过哪种方式获得MPMusicPlayerController的媒体源,可能都希望将每个媒体的信息显示出来,这时候可以通过MPMediaItem对象获得。一个MPMediaItem代表一个媒体文件,通过它可以访问媒体标题、专辑名称、专辑封面、音乐时长等等。无论是MPMediaQueue还是MPMediaItemCollection都有一个items属性,它是MPMediaItem数组,通过这个属性可以获得MPMediaItem对象。

下面就简单看一下MPMusicPlayerController的使用,在下面的例子中简单演示了音乐的选择、播放、暂停、通知、下一曲、上一曲功能,相信有了上面的概念,代码读起来并不复杂(示例中是直接通过MPMeidaPicker进行音乐选择的,但是仍然提供了两个方法getLocalMediaQuery和getLocalMediaItemCollection来演示如何直接通过MPMediaQueue获得媒体队列或媒体集合):

//
//  ViewController.m
//  MPMusicPlayerController
//
//  Created by Kenshin Cui 14/03/30
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//#import "ViewController.h"
#import <MediaPlayer/MediaPlayer.h>@interface ViewController ()<MPMediaPickerControllerDelegate>@property (nonatomic,strong) MPMediaPickerController *mediaPicker;//媒体选择控制器
@property (nonatomic,strong) MPMusicPlayerController *musicPlayer; //音乐播放器@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];
}-(void)dealloc{[self.musicPlayer endGeneratingPlaybackNotifications];
}/***  获得音乐播放器**  @return 音乐播放器*/
-(MPMusicPlayerController *)musicPlayer{if (!_musicPlayer) {_musicPlayer=[MPMusicPlayerController systemMusicPlayer];[_musicPlayer beginGeneratingPlaybackNotifications];//开启通知,否则监控不到MPMusicPlayerController的通知[self addNotification];//添加通知//如果不使用MPMediaPickerController可以使用如下方法获得音乐库媒体队列//[_musicPlayer setQueueWithItemCollection:[self getLocalMediaItemCollection]];}return _musicPlayer;
}/***  创建媒体选择器**  @return 媒体选择器*/
-(MPMediaPickerController *)mediaPicker{if (!_mediaPicker) {//初始化媒体选择器,这里设置媒体类型为音乐,其实这里也可以选择视频、广播等
//        _mediaPicker=[[MPMediaPickerController alloc]initWithMediaTypes:MPMediaTypeMusic];_mediaPicker=[[MPMediaPickerController alloc]initWithMediaTypes:MPMediaTypeAny];_mediaPicker.allowsPickingMultipleItems=YES;//允许多选
//        _mediaPicker.showsCloudItems=YES;//显示icloud选项_mediaPicker.prompt=@"请选择要播放的音乐";_mediaPicker.delegate=self;//设置选择器代理}return _mediaPicker;
}/***  取得媒体队列**  @return 媒体队列*/
-(MPMediaQuery *)getLocalMediaQuery{MPMediaQuery *mediaQueue=[MPMediaQuery songsQuery];for (MPMediaItem *item in mediaQueue.items) {NSLog(@"标题:%@,%@",item.title,item.albumTitle);}return mediaQueue;
}/***  取得媒体集合**  @return 媒体集合*/
-(MPMediaItemCollection *)getLocalMediaItemCollection{MPMediaQuery *mediaQueue=[MPMediaQuery songsQuery];NSMutableArray *array=[NSMutableArray array];for (MPMediaItem *item in mediaQueue.items) {[array addObject:item];NSLog(@"标题:%@,%@",item.title,item.albumTitle);}MPMediaItemCollection *mediaItemCollection=[[MPMediaItemCollection alloc]initWithItems:[array copy]];return mediaItemCollection;
}#pragma mark - MPMediaPickerController代理方法
//选择完成
-(void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection{MPMediaItem *mediaItem=[mediaItemCollection.items firstObject];//第一个播放音乐//注意很多音乐信息如标题、专辑、表演者、封面、时长等信息都可以通过MPMediaItem的valueForKey:方法得到,但是从iOS7开始都有对应的属性可以直接访问
//    NSString *title= [mediaItem valueForKey:MPMediaItemPropertyAlbumTitle];
//    NSString *artist= [mediaItem valueForKey:MPMediaItemPropertyAlbumArtist];
//    MPMediaItemArtwork *artwork= [mediaItem valueForKey:MPMediaItemPropertyArtwork];//UIImage *image=[artwork imageWithSize:CGSizeMake(100, 100)];//专辑图片NSLog(@"标题:%@,表演者:%@,专辑:%@",mediaItem.title ,mediaItem.artist,mediaItem.albumTitle);[self.musicPlayer setQueueWithItemCollection:mediaItemCollection];[self dismissViewControllerAnimated:YES completion:nil];
}
//取消选择
-(void)mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker{[self dismissViewControllerAnimated:YES completion:nil];
}#pragma mark - 通知
/***  添加通知*/
-(void)addNotification{NSNotificationCenter *notificationCenter=[NSNotificationCenter defaultCenter];[notificationCenter addObserver:self selector:@selector(playbackStateChange:) name:MPMusicPlayerControllerPlaybackStateDidChangeNotification object:self.musicPlayer];
}/***  播放状态改变通知**  @param notification 通知对象*/
-(void)playbackStateChange:(NSNotification *)notification{switch (self.musicPlayer.playbackState) {case MPMusicPlaybackStatePlaying:NSLog(@"正在播放...");break;case MPMusicPlaybackStatePaused:NSLog(@"播放暂停.");break;case MPMusicPlaybackStateStopped:NSLog(@"播放停止.");break;default:break;}
}#pragma mark - UI事件
- (IBAction)selectClick:(UIButton *)sender {[self presentViewController:self.mediaPicker animated:YES completion:nil];
}- (IBAction)playClick:(UIButton *)sender {[self.musicPlayer play];
}- (IBAction)puaseClick:(UIButton *)sender {[self.musicPlayer pause];
}- (IBAction)stopClick:(UIButton *)sender {[self.musicPlayer stop];
}- (IBAction)nextClick:(UIButton *)sender {[self.musicPlayer skipToNextItem];
}- (IBAction)prevClick:(UIButton *)sender {[self.musicPlayer skipToPreviousItem];
}@end

录音

除了上面说的,在AVFoundation框架中还要一个AVAudioRecorder类专门处理录音操作,它同样支持多种音频格式。与AVAudioPlayer类似,你完全可以将它看成是一个录音机控制类,下面是常用的属性和方法:

属性说明
@property(readonly, getter=isRecording) BOOL recording;是否正在录音,只读
@property(readonly) NSURL *url录音文件地址,只读
@property(readonly) NSDictionary *settings录音文件设置,只读
@property(readonly) NSTimeInterval currentTime录音时长,只读,注意仅仅在录音状态可用
@property(readonly) NSTimeInterval deviceCurrentTime输入设置的时间长度,只读,注意此属性一直可访问
@property(getter=isMeteringEnabled) BOOL meteringEnabled;是否启用录音测量,如果启用录音测量可以获得录音分贝等数据信息
@property(nonatomic, copy) NSArray *channelAssignments当前录音的通道
对象方法说明
- (instancetype)initWithURL:(NSURL *)url settings:(NSDictionary *)settings error:(NSError **)outError录音机对象初始化方法,注意其中的url必须是本地文件url,settings是录音格式、编码等设置
- (BOOL)prepareToRecord准备录音,主要用于创建缓冲区,如果不手动调用,在调用record录音时也会自动调用
- (BOOL)record开始录音
- (BOOL)recordAtTime:(NSTimeInterval)time在指定的时间开始录音,一般用于录音暂停再恢复录音
- (BOOL)recordForDuration:(NSTimeInterval) duration按指定的时长开始录音
- (BOOL)recordAtTime:(NSTimeInterval)time forDuration:(NSTimeInterval) duration在指定的时间开始录音,并指定录音时长
- (void)pause;暂停录音
- (void)stop;停止录音
- (BOOL)deleteRecording;删除录音,注意要删除录音此时录音机必须处于停止状态
- (void)updateMeters;更新测量数据,注意只有meteringEnabled为YES此方法才可用
- (float)peakPowerForChannel:(NSUInteger)channelNumber;指定通道的测量峰值,注意只有调用完updateMeters才有值
- (float)averagePowerForChannel:(NSUInteger)channelNumber指定通道的测量平均值,注意只有调用完updateMeters才有值
代理方法说明
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag完成录音
- (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError *)error录音编码发生错误

AVAudioRecorder很多属性和方法跟AVAudioPlayer都是类似的,但是它的创建有所不同,在创建录音机时除了指定路径外还必须指定录音设置信息,因为录音机必须知道录音文件的格式、采样率、通道数、每个采样点的位数等信息,但是也并不是所有的信息都必须设置,通常只需要几个常用设置。关于录音设置详见帮助文档中的“AV Foundation Audio Settings Constants”。

下面就使用AVAudioRecorder创建一个录音机,实现了录音、暂停、停止、播放等功能,实现效果大致如下:

AVAudioRecorderSnapshot

在这个示例中将实行一个完整的录音控制,包括录音、暂停、恢复、停止,同时还会实时展示用户录音的声音波动,当用户点击完停止按钮还会自动播放录音文件。程序的构建主要分为以下几步:

  1. 设置音频会话类型为AVAudioSessionCategoryPlayAndRecord,因为程序中牵扯到录音和播放操作。
  2. 创建录音机AVAudioRecorder,指定录音保存的路径并且设置录音属性,注意对于一般的录音文件要求的采样率、位数并不高,需要适当设置以保证录音文件的大小和效果。
  3. 设置录音机代理以便在录音完成后播放录音,打开录音测量保证能够实时获得录音时的声音强度。(注意声音强度范围-160到0,0代表最大输入)
  4. 创建音频播放器AVAudioPlayer,用于在录音完成之后播放录音。
  5. 创建一个定时器以便实时刷新录音测量值并更新录音强度到UIProgressView中显示。
  6. 添加录音、暂停、恢复、停止操作,需要注意录音的恢复操作其实是有音频会话管理的,恢复时只要再次调用record方法即可,无需手动管理恢复时间等。

下面是主要代码:

//
//  ViewController.m
//  AVAudioRecorder
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#define kRecordAudioFile @"myRecord.caf"@interface ViewController ()<AVAudioRecorderDelegate>@property (nonatomic,strong) AVAudioRecorder *audioRecorder;//音频录音机
@property (nonatomic,strong) AVAudioPlayer *audioPlayer;//音频播放器,用于播放录音文件
@property (nonatomic,strong) NSTimer *timer;//录音声波监控(注意这里暂时不对播放进行监控)@property (weak, nonatomic) IBOutlet UIButton *record;//开始录音
@property (weak, nonatomic) IBOutlet UIButton *pause;//暂停录音
@property (weak, nonatomic) IBOutlet UIButton *resume;//恢复录音
@property (weak, nonatomic) IBOutlet UIButton *stop;//停止录音
@property (weak, nonatomic) IBOutlet UIProgressView *audioPower;//音频波动@end@implementation ViewController#pragma mark - 控制器视图方法
- (void)viewDidLoad {[super viewDidLoad];[self setAudioSession];
}#pragma mark - 私有方法
/***  设置音频会话*/
-(void)setAudioSession{AVAudioSession *audioSession=[AVAudioSession sharedInstance];//设置为播放和录音状态,以便可以在录制完之后播放录音[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];[audioSession setActive:YES error:nil];
}/***  取得录音文件保存路径**  @return 录音文件路径*/
-(NSURL *)getSavePath{NSString *urlStr=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];urlStr=[urlStr stringByAppendingPathComponent:kRecordAudioFile];NSLog(@"file path:%@",urlStr);NSURL *url=[NSURL fileURLWithPath:urlStr];return url;
}/***  取得录音文件设置**  @return 录音设置*/
-(NSDictionary *)getAudioSetting{NSMutableDictionary *dicM=[NSMutableDictionary dictionary];//设置录音格式[dicM setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey];//设置录音采样率,8000是电话采样率,对于一般录音已经够了[dicM setObject:@(8000) forKey:AVSampleRateKey];//设置通道,这里采用单声道[dicM setObject:@(1) forKey:AVNumberOfChannelsKey];//每个采样点位数,分为8、16、24、32[dicM setObject:@(8) forKey:AVLinearPCMBitDepthKey];//是否使用浮点数采样[dicM setObject:@(YES) forKey:AVLinearPCMIsFloatKey];//....其他设置等return dicM;
}/***  获得录音机对象**  @return 录音机对象*/
-(AVAudioRecorder *)audioRecorder{if (!_audioRecorder) {//创建录音文件保存路径NSURL *url=[self getSavePath];//创建录音格式设置NSDictionary *setting=[self getAudioSetting];//创建录音机NSError *error=nil;_audioRecorder=[[AVAudioRecorder alloc]initWithURL:url settings:setting error:&error];_audioRecorder.delegate=self;_audioRecorder.meteringEnabled=YES;//如果要监控声波则必须设置为YESif (error) {NSLog(@"创建录音机对象时发生错误,错误信息:%@",error.localizedDescription);return nil;}}return _audioRecorder;
}/***  创建播放器**  @return 播放器*/
-(AVAudioPlayer *)audioPlayer{if (!_audioPlayer) {NSURL *url=[self getSavePath];NSError *error=nil;_audioPlayer=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];_audioPlayer.numberOfLoops=0;[_audioPlayer prepareToPlay];if (error) {NSLog(@"创建播放器过程中发生错误,错误信息:%@",error.localizedDescription);return nil;}}return _audioPlayer;
}/***  录音声波监控定制器**  @return 定时器*/
-(NSTimer *)timer{if (!_timer) {_timer=[NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:@selector(audioPowerChange) userInfo:nil repeats:YES];}return _timer;
}/***  录音声波状态设置*/
-(void)audioPowerChange{[self.audioRecorder updateMeters];//更新测量值float power= [self.audioRecorder averagePowerForChannel:0];//取得第一个通道的音频,注意音频强度范围时-160到0CGFloat progress=(1.0/160.0)*(power+160.0);[self.audioPower setProgress:progress];
}
#pragma mark - UI事件
/***  点击录音按钮**  @param sender 录音按钮*/
- (IBAction)recordClick:(UIButton *)sender {if (![self.audioRecorder isRecording]) {[self.audioRecorder record];//首次使用应用时如果调用record方法会询问用户是否允许使用麦克风self.timer.fireDate=[NSDate distantPast];}
}/***  点击暂定按钮**  @param sender 暂停按钮*/
- (IBAction)pauseClick:(UIButton *)sender {if ([self.audioRecorder isRecording]) {[self.audioRecorder pause];self.timer.fireDate=[NSDate distantFuture];}
}/***  点击恢复按钮*  恢复录音只需要再次调用record,AVAudioSession会帮助你记录上次录音位置并追加录音**  @param sender 恢复按钮*/
- (IBAction)resumeClick:(UIButton *)sender {[self recordClick:sender];
}/***  点击停止按钮**  @param sender 停止按钮*/
- (IBAction)stopClick:(UIButton *)sender {[self.audioRecorder stop];self.timer.fireDate=[NSDate distantFuture];self.audioPower.progress=0.0;
}#pragma mark - 录音机代理方法
/***  录音完成,录音完成后播放录音**  @param recorder 录音机对象*  @param flag     是否成功*/
-(void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag{if (![self.audioPlayer isPlaying]) {[self.audioPlayer play];}NSLog(@"录音完成!");
}@end

运行效果:

AVAudioRecorder

音频队列服务

大家应该已经注意到了,无论是前面的录音还是音频播放均不支持网络流媒体播放,当然对于录音来说这种需求可能不大,但是对于音频播放来说有时候就很有必要了。AVAudioPlayer只能播放本地文件,并且是一次性加载所以音频数据,初始化AVAudioPlayer时指定的URL也只能是File URL而不能是HTTP URL。当然,将音频文件下载到本地然后再调用AVAudioPlayer来播放也是一种播放网络音频的办法,但是这种方式最大的弊端就是必须等到整个音频播放完成才能播放,而不能使用流式播放,这往往在实际开发中是不切实际的。那么在iOS中如何播放网络流媒体呢?就是使用AudioToolbox框架中的音频队列服务Audio Queue Services。

使用音频队列服务完全可以做到音频播放和录制,首先看一下录音音频服务队列:

recording_architecture_2x

一个音频服务队列Audio Queue有三部分组成:

三个缓冲器Buffers:每个缓冲器都是一个存储音频数据的临时仓库。

一个缓冲队列Buffer Queue:一个包含音频缓冲器的有序队列。

一个回调Callback:一个自定义的队列回调函数。

声音通过输入设备进入缓冲队列中,首先填充第一个缓冲器;当第一个缓冲器填充满之后自动填充下一个缓冲器,同时会调用回调函数;在回调函数中需要将缓冲器中的音频数据写入磁盘,同时将缓冲器放回到缓冲队列中以便重用。下面是Apple官方关于音频队列服务的流程示意图:

recording_callback_function_2x

类似的,看一下音频播放缓冲队列,其组成部分和录音缓冲队列类似。

playback_architecture_2x

但是在音频播放缓冲队列中,回调函数调用的时机不同于音频录制缓冲队列,流程刚好相反。将音频读取到缓冲器中,一旦一个缓冲器填充满之后就放到缓冲队列中,然后继续填充其他缓冲器;当开始播放时,则从第一个缓冲器中读取音频进行播放;一旦播放完之后就会触发回调函数,开始播放下一个缓冲器中的音频,同时填充第一个缓冲器放;填充满之后再次放回到缓冲队列。下面是详细的流程:

playback_callback_function_2x

当然,要明白音频队列服务的原理并不难,问题是如何实现这个自定义的回调函数,这其中我们有大量的工作要做,控制播放状态、处理异常中断、进行音频编码等等。由于牵扯内容过多,而且不是本文目的,如果以后有时间将另开一篇文章重点介绍,目前有很多第三方优秀框架可以直接使用,例如AudioStreamer、FreeStreamer。由于前者当前只有非ARC版本,所以下面不妨使用FreeStreamer来简单演示在线音频播放的过程,当然在使用之前要做如下准备工作:

1.拷贝FreeStreamer中的Reachability.h、Reachability.m和Common、astreamer两个文件夹中的内容到项目中。

2.添加FreeStreamer使用的类库:CFNetwork.framework、AudioToolbox.framework、AVFoundation.framework
、libxml2.dylib、MediaPlayer.framework。

3.如果引用libxml2.dylib编译不通过,需要在Xcode的Targets-Build Settings-Header Build Path中添加$(SDKROOT)/usr/include/libxml2。

4.将FreeStreamer中的FreeStreamerMobile-Prefix.pch文件添加到项目中并将Targets-Build Settings-Precompile Prefix Header设置为YES,在Targets-Build Settings-Prefix Header设置为$(SRCROOT)/项目名称/FreeStreamerMobile-Prefix.pch(因为Xcode6默认没有pch文件)

然后就可以编写代码播放网络音频了:

<span style="color:#333333"><span style="color:green">//
//  ViewController.m
//  AudioQueueServices
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  使用FreeStreamer实现网络音频播放</span><span style="color:blue">#import </span><span style="color:#a31515">"ViewController.h"
</span><span style="color:blue">#import </span><span style="color:#a31515">"FSAudioStream.h"</span><span style="color:black">@</span><span style="color:blue">interface </span><span style="color:black">ViewController ()@</span><span style="color:blue">property </span><span style="color:black">(nonatomic,strong) FSAudioStream *audioStream;@end@implementation ViewController- (</span><span style="color:blue">void</span><span style="color:black">)viewDidLoad {[super viewDidLoad];[self.audioStream play];
}</span><span style="color:green">/***  取得本地文件路径**  @return 文件路径*/
</span><span style="color:black">-(NSURL *)getFileUrl{NSString *urlStr=[[NSBundle mainBundle]pathForResource:@</span><span style="color:#a31515">"刘若英 - 原来你也在这里.mp3" </span><span style="color:black">ofType:nil];NSURL *url=[NSURL fileURLWithPath:urlStr];</span><span style="color:blue">return </span><span style="color:black">url;
}
-(NSURL *)getNetworkUrl{NSString *urlStr=@</span><span style="color:#a31515">"http://192.168.1.102/liu.mp3"</span><span style="color:black">;NSURL *url=[NSURL URLWithString:urlStr];</span><span style="color:blue">return </span><span style="color:black">url;
}</span><span style="color:green">/***  创建FSAudioStream对象**  @return FSAudioStream对象*/
</span><span style="color:black">-(FSAudioStream *)audioStream{</span><span style="color:blue">if </span><span style="color:black">(!_audioStream) {NSURL *url=[self getNetworkUrl];</span><span style="color:green">//创建FSAudioStream对象</span><span style="color:black">_audioStream=[[FSAudioStream alloc]initWithUrl:url];_audioStream.onFailure=^(FSAudioStreamError error,NSString *description){NSLog(@</span><span style="color:#a31515">"播放过程中发生错误,错误信息:%@"</span><span style="color:black">,description);};_audioStream.onCompletion=^(){NSLog(@</span><span style="color:#a31515">"播放完成!"</span><span style="color:black">);};[_audioStream setVolume:0.5];</span><span style="color:green">//设置声音</span><span style="color:black">}</span><span style="color:blue">return </span><span style="color:black">_audioStream;
}@end</span></span>

其实FreeStreamer的功能很强大,不仅仅是播放本地、网络音频那么简单,它还支持播放列表、检查包内容、RSS订阅、播放中断等很多强大的功能,甚至还包含了一个音频分析器,有兴趣的朋友可以访问官网查看详细用法

视频

MPMoviePlayerController

在iOS中播放视频可以使用MediaPlayer.framework种的MPMoviePlayerController类来完成,它支持本地视频和网络视频播放。这个类实现了MPMediaPlayback协议,因此具备一般的播放器控制功能,例如播放、暂停、停止等。但是MPMediaPlayerController自身并不是一个完整的视图控制器,如果要在UI中展示视频需要将view属性添加到界面中。下面列出了MPMoviePlayerController的常用属性和方法:

属性说明
@property (nonatomic, copy) NSURL *contentURL播放媒体URL,这个URL可以是本地路径,也可以是网络路径
@property (nonatomic, readonly) UIView *view播放器视图,如果要显示视频必须将此视图添加到控制器视图中
@property (nonatomic, readonly) UIView *backgroundView播放器背景视图
@property (nonatomic, readonly) MPMoviePlaybackState playbackState媒体播放状态,枚举类型:
MPMoviePlaybackStateStopped:停止播放
MPMoviePlaybackStatePlaying:正在播放
MPMoviePlaybackStatePaused:暂停
MPMoviePlaybackStateInterrupted:中断
MPMoviePlaybackStateSeekingForward:向前定位
MPMoviePlaybackStateSeekingBackward:向后定位
@property (nonatomic, readonly) MPMovieLoadState loadState网络媒体加载状态,枚举类型:
MPMovieLoadStateUnknown:位置类型
MPMovieLoadStatePlayable:
MPMovieLoadStatePlaythroughOK:这种状态如果shouldAutoPlay为YES将自动播放
MPMovieLoadStateStalled:停滞状态
@property (nonatomic) MPMovieControlStyle controlStyle控制面板风格,枚举类型:
MPMovieControlStyleNone:无控制面板 
MPMovieControlStyleEmbedded:嵌入视频风格 
MPMovieControlStyleFullscreen:全屏 
MPMovieControlStyleDefault:默认风格
@property (nonatomic) MPMovieRepeatMode repeatMode;重复播放模式,枚举类型:
MPMovieRepeatModeNone:不重复,默认值
MPMovieRepeatModeOne:重复播放
@property (nonatomic) BOOL shouldAutoplay当网络媒体缓存到一定数据时是否自动播放,默认为YES
@property (nonatomic, getter=isFullscreen) BOOL fullscreen是否全屏展示,默认为NO,注意如果要通过此属性设置全屏必须在视图显示完成后设置,否则无效
@property (nonatomic) MPMovieScalingMode scalingMode视频缩放填充模式,枚举类型:
MPMovieScalingModeNone:不进行任何缩放
MPMovieScalingModeAspectFit:固定缩放比例并且尽量全部展示视频,不会裁切视频
MPMovieScalingModeAspectFill:固定缩放比例并填充满整个视图展示,可能会裁切视频
MPMovieScalingModeFill:不固定缩放比例压缩填充整个视图,视频不会被裁切但是比例失衡
@property (nonatomic, readonly) BOOL readyForDisplay是否有相关媒体被播放
@property (nonatomic, readonly) MPMovieMediaTypeMask movieMediaTypes媒体类别,枚举类型:
MPMovieMediaTypeMaskNone:未知类型
MPMovieMediaTypeMaskVideo:视频
MPMovieMediaTypeMaskAudio:音频
@property (nonatomic) MPMovieSourceType movieSourceType媒体源,枚举类型:
MPMovieSourceTypeUnknown:未知来源
MPMovieSourceTypeFile:本地文件
MPMovieSourceTypeStreaming:流媒体(直播或点播)
@property (nonatomic, readonly) NSTimeInterval duration媒体时长,如果未知则返回0
@property (nonatomic, readonly) NSTimeInterval playableDuration媒体可播放时长,主要用于表示网络媒体已下载视频时长
@property (nonatomic, readonly) CGSize naturalSize视频实际尺寸,如果未知则返回CGSizeZero
@property (nonatomic) NSTimeInterval initialPlaybackTime起始播放时间
@property (nonatomic) NSTimeInterval endPlaybackTime终止播放时间
@property (nonatomic) BOOL allowsAirPlay是否允许无线播放,默认为YES
@property (nonatomic, readonly, getter=isAirPlayVideoActive) BOOL airPlayVideoActive当前媒体是否正在通过AirPlay播放
@property(nonatomic, readonly) BOOL isPreparedToPlay是否准备好播放
@property(nonatomic) NSTimeInterval currentPlaybackTime当前播放时间,单位:秒
@property(nonatomic) float currentPlaybackRate当前播放速度,如果暂停则为0,正常速度为1.0,非0数据表示倍率
对象方法说明
- (instancetype)initWithContentURL:(NSURL *)url使用指定的URL初始化媒体播放控制器对象
- (void)setFullscreen:(BOOL)fullscreen animated:(BOOL)animated设置视频全屏,注意如果要通过此方法设置全屏则必须在其视图显示之后设置,否则无效
- (void)requestThumbnailImagesAtTimes:(NSArray *)playbackTimes timeOption:(MPMovieTimeOption)option获取在指定播放时间的视频缩略图,第一个参数是获取缩略图的时间点数组;第二个参数代表时间点精度,枚举类型:
MPMovieTimeOptionNearestKeyFrame:时间点附近
MPMovieTimeOptionExact:准确时间
- (void)cancelAllThumbnailImageRequests取消所有缩略图获取请求
- (void)prepareToPlay准备播放,加载视频数据到缓存,当调用play方法时如果没有准备好会自动调用此方法
- (void)play开始播放
- (void)pause暂停播放
- (void)stop停止播放
- (void)beginSeekingForward向前定位
- (void)beginSeekingBackward向后定位
- (void)endSeeking停止快进/快退
通知说明
MPMoviePlayerScalingModeDidChangeNotification视频缩放填充模式发生改变
MPMoviePlayerPlaybackDidFinishNotification媒体播放完成或用户手动退出,具体完成原因可以通过通知userInfo中的key为MPMoviePlayerPlaybackDidFinishReasonUserInfoKey的对象获取
MPMoviePlayerPlaybackStateDidChangeNotification播放状态改变,可配合playbakcState属性获取具体状态
MPMoviePlayerLoadStateDidChangeNotification媒体网络加载状态改变
MPMoviePlayerNowPlayingMovieDidChangeNotification当前播放的媒体内容发生改变
MPMoviePlayerWillEnterFullscreenNotification将要进入全屏
MPMoviePlayerDidEnterFullscreenNotification进入全屏后
MPMoviePlayerWillExitFullscreenNotification将要退出全屏
MPMoviePlayerDidExitFullscreenNotification退出全屏后
MPMoviePlayerIsAirPlayVideoActiveDidChangeNotification当媒体开始通过AirPlay播放或者结束AirPlay播放
MPMoviePlayerReadyForDisplayDidChangeNotification视频显示状态改变
MPMovieMediaTypesAvailableNotification确定了媒体可用类型后
MPMovieSourceTypeAvailableNotification确定了媒体来源后
MPMovieDurationAvailableNotification确定了媒体播放时长后
MPMovieNaturalSizeAvailableNotification确定了媒体的实际尺寸后
MPMoviePlayerThumbnailImageRequestDidFinishNotification缩略图请求完成之后
MPMediaPlaybackIsPreparedToPlayDidChangeNotification做好播放准备后

注意MPMediaPlayerController的状态等信息并不是通过代理来和外界交互的,而是通过通知中心,因此从上面的列表中可以看到常用的一些通知。由于MPMoviePlayerController本身对于媒体播放做了深度的封装,使用起来就相当简单:创建MPMoviePlayerController对象,设置frame属性,将MPMoviePlayerController的view添加到控制器视图中。下面的示例中将创建一个播放控制器并添加播放状态改变及播放完成的通知:

<span style="color:#333333"><span style="color:green">//
//  ViewController.m
//  MPMoviePlayerController
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//</span><span style="color:blue">#import </span><span style="color:#a31515">"ViewController.h"
</span><span style="color:blue">#import </span><span style="color:#a31515"><MediaPlayer/MediaPlayer.h></span><span style="color:black">@</span><span style="color:blue">interface </span><span style="color:black">ViewController ()@</span><span style="color:blue">property </span><span style="color:black">(nonatomic,strong) MPMoviePlayerController *moviePlayer;</span><span style="color:green">//视频播放控制器</span><span style="color:black">@end@implementation ViewController</span><span style="color:blue">#pragma </span><span style="color:black">mark - 控制器视图方法
- (</span><span style="color:blue">void</span><span style="color:black">)viewDidLoad {[super viewDidLoad];</span><span style="color:green">//播放</span><span style="color:black">[self.moviePlayer play];</span><span style="color:green">//添加通知</span><span style="color:black">[self addNotification];}-(</span><span style="color:blue">void</span><span style="color:black">)dealloc{</span><span style="color:green">//移除所有通知监控</span><span style="color:black">[[NSNotificationCenter defaultCenter] removeObserver:self];
}</span><span style="color:blue">#pragma </span><span style="color:black">mark - 私有方法
</span><span style="color:green">/***  取得本地文件路径**  @return 文件路径*/
</span><span style="color:black">-(NSURL *)getFileUrl{NSString *urlStr=[[NSBundle mainBundle] pathForResource:@</span><span style="color:#a31515">"The New Look of OS X Yosemite.mp4" </span><span style="color:black">ofType:nil];NSURL *url=[NSURL fileURLWithPath:urlStr];</span><span style="color:blue">return </span><span style="color:black">url;
}</span><span style="color:green">/***  取得网络文件路径**  @return 文件路径*/
</span><span style="color:black">-(NSURL *)getNetworkUrl{NSString *urlStr=@</span><span style="color:#a31515">"http://192.168.1.161/The New Look of OS X Yosemite.mp4"</span><span style="color:black">;urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];NSURL *url=[NSURL URLWithString:urlStr];</span><span style="color:blue">return </span><span style="color:black">url;
}</span><span style="color:green">/***  创建媒体播放控制器**  @return 媒体播放控制器*/
</span><span style="color:black">-(MPMoviePlayerController *)moviePlayer{</span><span style="color:blue">if </span><span style="color:black">(!_moviePlayer) {NSURL *url=[self getNetworkUrl];_moviePlayer=[[MPMoviePlayerController alloc]initWithContentURL:url];_moviePlayer.view.frame=self.view.bounds;_moviePlayer.view.autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;[self.view addSubview:_moviePlayer.view];}</span><span style="color:blue">return </span><span style="color:black">_moviePlayer;
}</span><span style="color:green">/***  添加通知监控媒体播放控制器状态*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)addNotification{NSNotificationCenter *notificationCenter=[NSNotificationCenter defaultCenter];[notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackStateChange:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:self.moviePlayer];[notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackFinished:) name:MPMoviePlayerPlaybackDidFinishNotification object:self.moviePlayer];}</span><span style="color:green">/***  播放状态改变,注意播放完成时的状态是暂停**  @param notification 通知对象*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)mediaPlayerPlaybackStateChange:(NSNotification *)notification{</span><span style="color:blue">switch </span><span style="color:black">(self.moviePlayer.playbackState) {</span><span style="color:blue">case </span><span style="color:black">MPMoviePlaybackStatePlaying:NSLog(@</span><span style="color:#a31515">"正在播放..."</span><span style="color:black">);</span><span style="color:blue">break</span><span style="color:black">;</span><span style="color:blue">case </span><span style="color:black">MPMoviePlaybackStatePaused:NSLog(@</span><span style="color:#a31515">"暂停播放."</span><span style="color:black">);</span><span style="color:blue">break</span><span style="color:black">;</span><span style="color:blue">case </span><span style="color:black">MPMoviePlaybackStateStopped:NSLog(@</span><span style="color:#a31515">"停止播放."</span><span style="color:black">);</span><span style="color:blue">break</span><span style="color:black">;</span><span style="color:blue">default</span><span style="color:black">:NSLog(@</span><span style="color:#a31515">"播放状态:%li"</span><span style="color:black">,self.moviePlayer.playbackState);</span><span style="color:blue">break</span><span style="color:black">;}
}</span><span style="color:green">/***  播放完成**  @param notification 通知对象*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)mediaPlayerPlaybackFinished:(NSNotification *)notification{NSLog(@</span><span style="color:#a31515">"播放完成.%li"</span><span style="color:black">,self.moviePlayer.playbackState);
}@end</span></span>

运行效果:

MPMoviePlayerController
从上面的API大家也不难看出其实MPMoviePlayerController功能相当强大,日常开发中作为一般的媒体播放器也完全没有问题。MPMoviePlayerController除了一般的视频播放和控制外还有一些强大的功能,例如截取视频缩略图。请求视频缩略图时只要调用- (void)requestThumbnailImagesAtTimes:(NSArray *)playbackTimes timeOption:(MPMovieTimeOption)option方法指定获得缩略图的时间点,然后监控MPMoviePlayerThumbnailImageRequestDidFinishNotification通知,每个时间点的缩略图请求完成就会调用通知,在通知调用方法中可以通过MPMoviePlayerThumbnailImageKey获得UIImage对象处理即可。例如下面的程序演示了在程序启动后获得两个时间点的缩略图的过程,截图成功后保存到相册:

<span style="color:#333333"><span style="color:green">//
//  ViewController.m
//  MPMoviePlayerController
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  视频截图</span><span style="color:blue">#import </span><span style="color:#a31515">"ViewController.h"
</span><span style="color:blue">#import </span><span style="color:#a31515"><MediaPlayer/MediaPlayer.h></span><span style="color:black">@</span><span style="color:blue">interface </span><span style="color:black">ViewController ()@</span><span style="color:blue">property </span><span style="color:black">(nonatomic,strong) MPMoviePlayerController *moviePlayer;</span><span style="color:green">//视频播放控制器</span><span style="color:black">@end@implementation ViewController</span><span style="color:blue">#pragma </span><span style="color:black">mark - 控制器视图方法
- (</span><span style="color:blue">void</span><span style="color:black">)viewDidLoad {[super viewDidLoad];</span><span style="color:green">//播放</span><span style="color:black">[self.moviePlayer play];</span><span style="color:green">//添加通知</span><span style="color:black">[self addNotification];</span><span style="color:green">//获取缩略图</span><span style="color:black">[self thumbnailImageRequest];
}-(</span><span style="color:blue">void</span><span style="color:black">)dealloc{</span><span style="color:green">//移除所有通知监控</span><span style="color:black">[[NSNotificationCenter defaultCenter] removeObserver:self];
}</span><span style="color:blue">#pragma </span><span style="color:black">mark - 私有方法
</span><span style="color:green">/***  取得本地文件路径**  @return 文件路径*/
</span><span style="color:black">-(NSURL *)getFileUrl{NSString *urlStr=[[NSBundle mainBundle] pathForResource:@</span><span style="color:#a31515">"The New Look of OS X Yosemite.mp4" </span><span style="color:black">ofType:nil];NSURL *url=[NSURL fileURLWithPath:urlStr];</span><span style="color:blue">return </span><span style="color:black">url;
}</span><span style="color:green">/***  取得网络文件路径**  @return 文件路径*/
</span><span style="color:black">-(NSURL *)getNetworkUrl{NSString *urlStr=@</span><span style="color:#a31515">"http://192.168.1.161/The New Look of OS X Yosemite.mp4"</span><span style="color:black">;urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];NSURL *url=[NSURL URLWithString:urlStr];</span><span style="color:blue">return </span><span style="color:black">url;
}</span><span style="color:green">/***  创建媒体播放控制器**  @return 媒体播放控制器*/
</span><span style="color:black">-(MPMoviePlayerController *)moviePlayer{</span><span style="color:blue">if </span><span style="color:black">(!_moviePlayer) {NSURL *url=[self getNetworkUrl];_moviePlayer=[[MPMoviePlayerController alloc]initWithContentURL:url];_moviePlayer.view.frame=self.view.bounds;_moviePlayer.view.autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;[self.view addSubview:_moviePlayer.view];}</span><span style="color:blue">return </span><span style="color:black">_moviePlayer;
}</span><span style="color:green">/***  获取视频缩略图*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)thumbnailImageRequest{</span><span style="color:green">//获取13.0s、21.5s的缩略图</span><span style="color:black">[self.moviePlayer requestThumbnailImagesAtTimes:@[@13.0,@21.5] timeOption:MPMovieTimeOptionNearestKeyFrame];
}</span><span style="color:blue">#pragma </span><span style="color:black">mark - 控制器通知
</span><span style="color:green">/***  添加通知监控媒体播放控制器状态*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)addNotification{NSNotificationCenter *notificationCenter=[NSNotificationCenter defaultCenter];[notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackStateChange:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:self.moviePlayer];[notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackFinished:) name:MPMoviePlayerPlaybackDidFinishNotification object:self.moviePlayer];[notificationCenter addObserver:self selector:@selector(mediaPlayerThumbnailRequestFinished:) name:MPMoviePlayerThumbnailImageRequestDidFinishNotification object:self.moviePlayer];}</span><span style="color:green">/***  播放状态改变,注意播放完成时的状态是暂停**  @param notification 通知对象*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)mediaPlayerPlaybackStateChange:(NSNotification *)notification{</span><span style="color:blue">switch </span><span style="color:black">(self.moviePlayer.playbackState) {</span><span style="color:blue">case </span><span style="color:black">MPMoviePlaybackStatePlaying:NSLog(@</span><span style="color:#a31515">"正在播放..."</span><span style="color:black">);</span><span style="color:blue">break</span><span style="color:black">;</span><span style="color:blue">case </span><span style="color:black">MPMoviePlaybackStatePaused:NSLog(@</span><span style="color:#a31515">"暂停播放."</span><span style="color:black">);</span><span style="color:blue">break</span><span style="color:black">;</span><span style="color:blue">case </span><span style="color:black">MPMoviePlaybackStateStopped:NSLog(@</span><span style="color:#a31515">"停止播放."</span><span style="color:black">);</span><span style="color:blue">break</span><span style="color:black">;</span><span style="color:blue">default</span><span style="color:black">:NSLog(@</span><span style="color:#a31515">"播放状态:%li"</span><span style="color:black">,self.moviePlayer.playbackState);</span><span style="color:blue">break</span><span style="color:black">;}
}</span><span style="color:green">/***  播放完成**  @param notification 通知对象*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)mediaPlayerPlaybackFinished:(NSNotification *)notification{NSLog(@</span><span style="color:#a31515">"播放完成.%li"</span><span style="color:black">,self.moviePlayer.playbackState);
}</span><span style="color:green">/***  缩略图请求完成,此方法每次截图成功都会调用一次**  @param notification 通知对象*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)mediaPlayerThumbnailRequestFinished:(NSNotification *)notification{NSLog(@</span><span style="color:#a31515">"视频截图完成."</span><span style="color:black">);UIImage *image=notification.userInfo[MPMoviePlayerThumbnailImageKey];</span><span style="color:green">//保存图片到相册(首次调用会请求用户获得访问相册权限)</span><span style="color:black">UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
}@end</span></span>

截图效果:

MPMoviePlayerController_Thumbnail1     MPMoviePlayerController_Thumbnail2

扩展--使用AVFoundation生成缩略图

通过前面的方法大家应该已经看到,使用MPMoviePlayerController来生成缩略图足够简单,但是如果仅仅是是为了生成缩略图而不进行视频播放的话,此刻使用MPMoviePlayerController就有点大材小用了。其实使用AVFundation框架中的AVAssetImageGenerator就可以获取视频缩略图。使用AVAssetImageGenerator获取缩略图大致分为三个步骤:

  1. 创建AVURLAsset对象(此类主要用于获取媒体信息,包括视频、声音等)。
  2. 根据AVURLAsset创建AVAssetImageGenerator对象。
  3. 使用AVAssetImageGenerator的copyCGImageAtTime::方法获得指定时间点的截图。
<span style="color:#333333"><span style="color:green">//
//  ViewController.m
//  AVAssetImageGenerator
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//</span><span style="color:blue">#import </span><span style="color:#a31515">"ViewController.h"
</span><span style="color:blue">#import </span><span style="color:#a31515"><AVFoundation/AVFoundation.h></span><span style="color:black">@</span><span style="color:blue">interface </span><span style="color:black">ViewController ()@end@implementation ViewController- (</span><span style="color:blue">void</span><span style="color:black">)viewDidLoad {[super viewDidLoad];</span><span style="color:green">//获取第13.0s的缩略图</span><span style="color:black">[self thumbnailImageRequest:13.0];
}</span><span style="color:blue">#pragma </span><span style="color:black">mark - 私有方法
</span><span style="color:green">/***  取得本地文件路径**  @return 文件路径*/
</span><span style="color:black">-(NSURL *)getFileUrl{NSString *urlStr=[[NSBundle mainBundle] pathForResource:@</span><span style="color:#a31515">"The New Look of OS X Yosemite.mp4" </span><span style="color:black">ofType:nil];NSURL *url=[NSURL fileURLWithPath:urlStr];</span><span style="color:blue">return </span><span style="color:black">url;
}</span><span style="color:green">/***  取得网络文件路径**  @return 文件路径*/
</span><span style="color:black">-(NSURL *)getNetworkUrl{NSString *urlStr=@</span><span style="color:#a31515">"http://192.168.1.161/The New Look of OS X Yosemite.mp4"</span><span style="color:black">;urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];NSURL *url=[NSURL URLWithString:urlStr];</span><span style="color:blue">return </span><span style="color:black">url;
}</span><span style="color:green">/***  截取指定时间的视频缩略图**  @param timeBySecond 时间点*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)thumbnailImageRequest:(CGFloat )timeBySecond{</span><span style="color:green">//创建URL</span><span style="color:black">NSURL *url=[self getNetworkUrl];</span><span style="color:green">//根据url创建AVURLAsset</span><span style="color:black">AVURLAsset *urlAsset=[AVURLAsset assetWithURL:url];</span><span style="color:green">//根据AVURLAsset创建AVAssetImageGenerator</span><span style="color:black">AVAssetImageGenerator *imageGenerator=[AVAssetImageGenerator assetImageGeneratorWithAsset:urlAsset];</span><span style="color:green">/*截图* requestTime:缩略图创建时间* actualTime:缩略图实际生成的时间*/</span><span style="color:black">NSError *error=nil;CMTime time=CMTimeMakeWithSeconds(timeBySecond, 10);</span><span style="color:green">//CMTime是表示电影时间信息的结构体,第一个参数表示是视频第几秒,第二个参数表示每秒帧数.(如果要活的某一秒的第几帧可以使用CMTimeMake方法)</span><span style="color:black">CMTime actualTime;CGImageRef cgImage= [imageGenerator copyCGImageAtTime:time actualTime:&actualTime error:&error];</span><span style="color:blue">if</span><span style="color:black">(error){NSLog(@</span><span style="color:#a31515">"截取视频缩略图时发生错误,错误信息:%@"</span><span style="color:black">,error.localizedDescription);</span><span style="color:blue">return</span><span style="color:black">;}CMTimeShow(actualTime);UIImage *image=[UIImage imageWithCGImage:cgImage];</span><span style="color:green">//转化为UIImage//保存到相册</span><span style="color:black">UIImageWriteToSavedPhotosAlbum(image,nil, nil, nil);CGImageRelease(cgImage);
}@end</span></span>

生成的缩略图效果:

AVAssetImageGenerator_Thumbnail

MPMoviePlayerViewController

其实MPMoviePlayerController如果不作为嵌入视频来播放(例如在新闻中嵌入一个视频),通常在播放时都是占满一个屏幕的,特别是在iPhone、iTouch上。因此从iOS3.2以后苹果也在思考既然MPMoviePlayerController在使用时通常都是将其视图view添加到另外一个视图控制器中作为子视图,那么何不直接创建一个控制器视图内部创建一个MPMoviePlayerController属性并且默认全屏播放,开发者在开发的时候直接使用这个视图控制器。这个内部有一个MPMoviePlayerController的视图控制器就是MPMoviePlayerViewController,它继承于UIViewController。MPMoviePlayerViewController内部多了一个moviePlayer属性和一个带有url的初始化方法,同时它内部实现了一些作为模态视图展示所特有的功能,例如默认是全屏模式展示、弹出后自动播放、作为模态窗口展示时如果点击“Done”按钮会自动退出模态窗口等。在下面的示例中就不直接将播放器放到主视图控制器,而是放到一个模态视图控制器中,简单演示MPMoviePlayerViewController的使用。

<span style="color:#333333"><span style="color:green">//
//  ViewController.m
//  MPMoviePlayerViewController
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  MPMoviePlayerViewController使用</span><span style="color:blue">#import </span><span style="color:#a31515">"ViewController.h"
</span><span style="color:blue">#import </span><span style="color:#a31515"><MediaPlayer/MediaPlayer.h></span><span style="color:black">@</span><span style="color:blue">interface </span><span style="color:black">ViewController ()</span><span style="color:green">//播放器视图控制器
</span><span style="color:black">@</span><span style="color:blue">property </span><span style="color:black">(nonatomic,strong) MPMoviePlayerViewController *moviePlayerViewController;@end@implementation ViewController</span><span style="color:blue">#pragma </span><span style="color:black">mark - 控制器视图方法
- (</span><span style="color:blue">void</span><span style="color:black">)viewDidLoad {[super viewDidLoad];}-(</span><span style="color:blue">void</span><span style="color:black">)dealloc{</span><span style="color:green">//移除所有通知监控</span><span style="color:black">[[NSNotificationCenter defaultCenter] removeObserver:self];
}</span><span style="color:blue">#pragma </span><span style="color:black">mark - 私有方法
</span><span style="color:green">/***  取得本地文件路径**  @return 文件路径*/
</span><span style="color:black">-(NSURL *)getFileUrl{NSString *urlStr=[[NSBundle mainBundle] pathForResource:@</span><span style="color:#a31515">"The New Look of OS X Yosemite.mp4" </span><span style="color:black">ofType:nil];NSURL *url=[NSURL fileURLWithPath:urlStr];</span><span style="color:blue">return </span><span style="color:black">url;
}</span><span style="color:green">/***  取得网络文件路径**  @return 文件路径*/
</span><span style="color:black">-(NSURL *)getNetworkUrl{NSString *urlStr=@</span><span style="color:#a31515">"http://192.168.1.161/The New Look of OS X Yosemite.mp4"</span><span style="color:black">;urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];NSURL *url=[NSURL URLWithString:urlStr];</span><span style="color:blue">return </span><span style="color:black">url;
}-(MPMoviePlayerViewController *)moviePlayerViewController{</span><span style="color:blue">if </span><span style="color:black">(!_moviePlayerViewController) {NSURL *url=[self getNetworkUrl];_moviePlayerViewController=[[MPMoviePlayerViewController alloc]initWithContentURL:url];[self addNotification];}</span><span style="color:blue">return </span><span style="color:black">_moviePlayerViewController;
}
</span><span style="color:blue">#pragma </span><span style="color:black">mark - UI事件
- (IBAction)playClick:(UIButton *)sender {self.moviePlayerViewController=nil;</span><span style="color:green">//保证每次点击都重新创建视频播放控制器视图,避免再次点击时由于不播放的问题
//    [self presentViewController:self.moviePlayerViewController animated:YES completion:nil];//注意,在MPMoviePlayerViewController.h中对UIViewController扩展两个用于模态展示和关闭MPMoviePlayerViewController的方法,增加了一种下拉展示动画效果</span><span style="color:black">[self presentMoviePlayerViewControllerAnimated:self.moviePlayerViewController];
}</span><span style="color:blue">#pragma </span><span style="color:black">mark - 控制器通知
</span><span style="color:green">/***  添加通知监控媒体播放控制器状态*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)addNotification{NSNotificationCenter *notificationCenter=[NSNotificationCenter defaultCenter];[notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackStateChange:) name:MPMoviePlayerPlaybackStateDidChangeNotification object:self.moviePlayerViewController.moviePlayer];[notificationCenter addObserver:self selector:@selector(mediaPlayerPlaybackFinished:) name:MPMoviePlayerPlaybackDidFinishNotification object:self.moviePlayerViewController.moviePlayer];}</span><span style="color:green">/***  播放状态改变,注意播放完成时的状态是暂停**  @param notification 通知对象*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)mediaPlayerPlaybackStateChange:(NSNotification *)notification{</span><span style="color:blue">switch </span><span style="color:black">(self.moviePlayerViewController.moviePlayer.playbackState) {</span><span style="color:blue">case </span><span style="color:black">MPMoviePlaybackStatePlaying:NSLog(@</span><span style="color:#a31515">"正在播放..."</span><span style="color:black">);</span><span style="color:blue">break</span><span style="color:black">;</span><span style="color:blue">case </span><span style="color:black">MPMoviePlaybackStatePaused:NSLog(@</span><span style="color:#a31515">"暂停播放."</span><span style="color:black">);</span><span style="color:blue">break</span><span style="color:black">;</span><span style="color:blue">case </span><span style="color:black">MPMoviePlaybackStateStopped:NSLog(@</span><span style="color:#a31515">"停止播放."</span><span style="color:black">);</span><span style="color:blue">break</span><span style="color:black">;</span><span style="color:blue">default</span><span style="color:black">:NSLog(@</span><span style="color:#a31515">"播放状态:%li"</span><span style="color:black">,self.moviePlayerViewController.moviePlayer.playbackState);</span><span style="color:blue">break</span><span style="color:black">;}
}</span><span style="color:green">/***  播放完成**  @param notification 通知对象*/
</span><span style="color:black">-(</span><span style="color:blue">void</span><span style="color:black">)mediaPlayerPlaybackFinished:(NSNotification *)notification{NSLog(@</span><span style="color:#a31515">"播放完成.%li"</span><span style="color:black">,self.moviePlayerViewController.moviePlayer.playbackState);
}@end</span></span>

运行效果:

MPMoviePlayerViewController

这里需要强调一下,由于MPMoviePlayerViewController的初始化方法做了大量工作(例如设置URL、自动播放、添加点击Done完成的监控等),所以当再次点击播放弹出新的模态窗口的时如果不销毁之前的MPMoviePlayerViewController,那么新的对象就无法完成初始化,这样也就不能再次进行播放。

AVPlayer

MPMoviePlayerController足够强大,几乎不用写几行代码就能完成一个播放器,但是正是由于它的高度封装使得要自定义这个播放器变得很复杂,甚至是不可能完成。例如有些时候需要自定义播放器的样式,那么如果要使用MPMoviePlayerController就不合适了,如果要对视频有自由的控制则可以使用AVPlayer。AVPlayer存在于AVFoundation中,它更加接近于底层,所以灵活性也更强:

AVFoundation_Framework

AVPlayer本身并不能显示视频,而且它也不像MPMoviePlayerController有一个view属性。如果AVPlayer要显示必须创建一个播放器层AVPlayerLayer用于展示,播放器层继承于CALayer,有了AVPlayerLayer之添加到控制器视图的layer中即可。要使用AVPlayer首先了解一下几个常用的类:

AVAsset:主要用于获取多媒体信息,是一个抽象类,不能直接使用。

AVURLAsset:AVAsset的子类,可以根据一个URL路径创建一个包含媒体信息的AVURLAsset对象。

AVPlayerItem:一个媒体资源管理对象,管理者视频的一些基本信息和状态,一个AVPlayerItem对应着一个视频资源。

下面简单通过一个播放器来演示AVPlayer的使用,播放器的效果如下:

AVPlayer_Thumbnail

在这个自定义的播放器中实现了视频播放、暂停、进度展示和视频列表功能,下面将对这些功能一一介绍。

首先说一下视频的播放、暂停功能,这也是最基本的功能,AVPlayer对应着两个方法play、pause来实现。但是关键问题是如何判断当前视频是否在播放,在前面的内容中无论是音频播放器还是视频播放器都有对应的状态来判断,但是AVPlayer却没有这样的状态属性,通常情况下可以通过判断播放器的播放速度来获得播放状态。如果rate为0说明是停止状态,1是则是正常播放状态。

其次要展示播放进度就没有其他播放器那么简单了。在前面的播放器中通常是使用通知来获得播放器的状态,媒体加载状态等,但是无论是AVPlayer还是AVPlayerItem(AVPlayer有一个属性currentItem是AVPlayerItem类型,表示当前播放的视频对象)都无法获得这些信息。当然AVPlayerItem是有通知的,但是对于获得播放状态和加载状态有用的通知只有一个:播放完成通知AVPlayerItemDidPlayToEndTimeNotification。在播放视频时,特别是播放网络视频往往需要知道视频加载情况、缓冲情况、播放情况,这些信息可以通过KVO监控AVPlayerItem的status、loadedTimeRanges属性来获得。当AVPlayerItem的status属性为AVPlayerStatusReadyToPlay是说明正在播放,只有处于这个状态时才能获得视频时长等信息;当loadedTimeRanges的改变时(每缓冲一部分数据就会更新此属性)可以获得本次缓冲加载的视频范围(包含起始时间、本次加载时长),这样一来就可以实时获得缓冲情况。然后就是依靠AVPlayer的- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block方法获得播放进度,这个方法会在设定的时间间隔内定时更新播放进度,通过time参数通知客户端。相信有了这些视频信息播放进度就不成问题了,事实上通过这些信息就算是平时看到的其他播放器的缓冲进度显示以及拖动播放的功能也可以顺利的实现。

最后就是视频切换的功能,在前面介绍的所有播放器中每个播放器对象一次只能播放一个视频,如果要切换视频只能重新创建一个对象,但是AVPlayer却提供了- (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)item方法用于在不同的视频之间切换(事实上在AVFoundation内部还有一个AVQueuePlayer专门处理播放列表切换,有兴趣的朋友可以自行研究,这里不再赘述)。

下面附上代码:

//
//  ViewController.m
//  AVPlayer
//
//  Created by Kenshin Cui on 14/03/30.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>@interface ViewController ()@property (nonatomic,strong) AVPlayer *player;//播放器对象@property (weak, nonatomic) IBOutlet UIView *container; //播放器容器
@property (weak, nonatomic) IBOutlet UIButton *playOrPause; //播放/暂停按钮
@property (weak, nonatomic) IBOutlet UIProgressView *progress;//播放进度@end@implementation ViewController#pragma mark - 控制器视图方法
- (void)viewDidLoad {[super viewDidLoad];[self setupUI];[self.player play];
}-(void)dealloc{[self removeObserverFromPlayerItem:self.player.currentItem];[self removeNotification];
}#pragma mark - 私有方法
-(void)setupUI{//创建播放器层AVPlayerLayer *playerLayer=[AVPlayerLayer playerLayerWithPlayer:self.player];playerLayer.frame=self.container.frame;//playerLayer.videoGravity=AVLayerVideoGravityResizeAspect;//视频填充模式[self.container.layer addSublayer:playerLayer];
}/***  截取指定时间的视频缩略图**  @param timeBySecond 时间点*//***  初始化播放器**  @return 播放器对象*/
-(AVPlayer *)player{if (!_player) {AVPlayerItem *playerItem=[self getPlayItem:0];_player=[AVPlayer playerWithPlayerItem:playerItem];[self addProgressObserver];[self addObserverToPlayerItem:playerItem];}return _player;
}/***  根据视频索引取得AVPlayerItem对象**  @param videoIndex 视频顺序索引**  @return AVPlayerItem对象*/
-(AVPlayerItem *)getPlayItem:(int)videoIndex{NSString *urlStr=[NSString stringWithFormat:@"http://192.168.1.161/%i.mp4",videoIndex];urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];NSURL *url=[NSURL URLWithString:urlStr];AVPlayerItem *playerItem=[AVPlayerItem playerItemWithURL:url];return playerItem;
}
#pragma mark - 通知
/***  添加播放器通知*/
-(void)addNotification{//给AVPlayerItem添加播放完成通知[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem];
}-(void)removeNotification{[[NSNotificationCenter defaultCenter] removeObserver:self];
}/***  播放完成通知**  @param notification 通知对象*/
-(void)playbackFinished:(NSNotification *)notification{NSLog(@"视频播放完成.");
}#pragma mark - 监控
/***  给播放器添加进度更新*/
-(void)addProgressObserver{AVPlayerItem *playerItem=self.player.currentItem;UIProgressView *progress=self.progress;//这里设置每秒执行一次[self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {float current=CMTimeGetSeconds(time);float total=CMTimeGetSeconds([playerItem duration]);NSLog(@"当前已经播放%.2fs.",current);if (current) {[progress setProgress:(current/total) animated:YES];}}];
}/***  给AVPlayerItem添加监控**  @param playerItem AVPlayerItem对象*/
-(void)addObserverToPlayerItem:(AVPlayerItem *)playerItem{//监控状态属性,注意AVPlayer也有一个status属性,通过监控它的status也可以获得播放状态[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];//监控网络加载情况属性[playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
}
-(void)removeObserverFromPlayerItem:(AVPlayerItem *)playerItem{[playerItem removeObserver:self forKeyPath:@"status"];[playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
}
/***  通过KVO监控播放器状态**  @param keyPath 监控属性*  @param object  监视器*  @param change  状态改变*  @param context 上下文*/
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{AVPlayerItem *playerItem=object;if ([keyPath isEqualToString:@"status"]) {AVPlayerStatus status= [[change objectForKey:@"new"] intValue];if(status==AVPlayerStatusReadyToPlay){NSLog(@"正在播放...,视频总长度:%.2f",CMTimeGetSeconds(playerItem.duration));}}else if([keyPath isEqualToString:@"loadedTimeRanges"]){NSArray *array=playerItem.loadedTimeRanges;CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次缓冲时间范围float startSeconds = CMTimeGetSeconds(timeRange.start);float durationSeconds = CMTimeGetSeconds(timeRange.duration);NSTimeInterval totalBuffer = startSeconds + durationSeconds;//缓冲总长度NSLog(@"共缓冲:%.2f",totalBuffer);
//}
}#pragma mark - UI事件
/***  点击播放/暂停按钮**  @param sender 播放/暂停按钮*/
- (IBAction)playClick:(UIButton *)sender {
//    AVPlayerItemDidPlayToEndTimeNotification//AVPlayerItem *playerItem= self.player.currentItem;if(self.player.rate==0){ //说明时暂停[sender setImage:[UIImage imageNamed:@"player_pause"] forState:UIControlStateNormal];[self.player play];}else if(self.player.rate==1){//正在播放[self.player pause];[sender setImage:[UIImage imageNamed:@"player_play"] forState:UIControlStateNormal];}
}/***  切换选集,这里使用按钮的tag代表视频名称**  @param sender 点击按钮对象*/
- (IBAction)navigationButtonClick:(UIButton *)sender {[self removeNotification];[self removeObserverFromPlayerItem:self.player.currentItem];AVPlayerItem *playerItem=[self getPlayItem:sender.tag];[self addObserverToPlayerItem:playerItem];//切换视频[self.player replaceCurrentItemWithPlayerItem:playerItem];[self addNotification];
}@end

运行效果:

AVPlayer

到目前为止无论是MPMoviePlayerController还是AVPlayer来播放视频都相当强大,但是它也存在着一些不可回避的问题,那就是支持的视频编码格式很有限:H.264、MPEG-4,扩展名(压缩格式):.mp4、.mov、.m4v、.m2v、.3gp、.3g2等。但是无论是MPMoviePlayerController还是AVPlayer它们都支持绝大多数音频编码,所以大家如果纯粹是为了播放音乐的话也可以考虑使用这两个播放器。那么如何支持更多视频编码格式呢?目前来说主要还是依靠第三方框架,在iOS上常用的视频编码、解码框架有:VLC、ffmpeg, 具体使用方式今天就不再做详细介绍。

摄像头

UIImagePickerController拍照和视频录制

下面看一下在iOS如何拍照和录制视频。在iOS中要拍照和录制视频最简单的方法就是使用UIImagePickerController。UIImagePickerController继承于UINavigationController,前面的文章中主要使用它来选取照片,其实UIImagePickerController的功能不仅如此,它还可以用来拍照和录制视频。首先看一下这个类常用的属性和方法:

属性说明
@property(nonatomic)           UIImagePickerControllerSourceType     sourceType拾取源类型,sourceType是枚举类型:
UIImagePickerControllerSourceTypePhotoLibrary:照片库
,默认值
UIImagePickerControllerSourceTypeCamera:摄像头
UIImagePickerControllerSourceTypeSavedPhotosAlbum:相簿
@property(nonatomic,copy)      NSArray                              *mediaTypes媒体类型,默认情况下此数组包含kUTTypeImage,所以拍照时可以不用设置;但是当要录像的时候必须设置,可以设置为kUTTypeVideo(视频,但不带声音)或者kUTTypeMovie(视频并带有声音)
@property(nonatomic)           NSTimeInterval                        videoMaximumDuration视频最大录制时长,默认为10 s
@property(nonatomic)           UIImagePickerControllerQualityType    videoQuality视频质量,枚举类型:
UIImagePickerControllerQualityTypeHigh:高清质量
UIImagePickerControllerQualityTypeMedium:中等质量,适合WiFi传输
UIImagePickerControllerQualityTypeLow:低质量,适合蜂窝网传输
UIImagePickerControllerQualityType640x480:640*480
UIImagePickerControllerQualityTypeIFrame1280x720:1280*720
UIImagePickerControllerQualityTypeIFrame960x540:960*540
@property(nonatomic)           BOOL                                  showsCameraControls是否显示摄像头控制面板,默认为YES
@property(nonatomic,retain)    UIView                                *cameraOverlayView摄像头上覆盖的视图,可用通过这个视频来自定义拍照或录像界面
@property(nonatomic)           CGAffineTransform                     cameraViewTransform摄像头形变
@property(nonatomic) UIImagePickerControllerCameraCaptureMode cameraCaptureMode摄像头捕获模式,捕获模式是枚举类型:
UIImagePickerControllerCameraCaptureModePhoto:拍照模式
UIImagePickerControllerCameraCaptureModeVideo:视频录制模式
@property(nonatomic) UIImagePickerControllerCameraDevice      cameraDevice摄像头设备,cameraDevice是枚举类型:
UIImagePickerControllerCameraDeviceRear:前置摄像头
UIImagePickerControllerCameraDeviceFront:后置摄像头
@property(nonatomic) UIImagePickerControllerCameraFlashMode   cameraFlashMode闪光灯模式,枚举类型:
UIImagePickerControllerCameraFlashModeOff:关闭闪光灯
UIImagePickerControllerCameraFlashModeAuto:闪光灯自动
UIImagePickerControllerCameraFlashModeOn:打开闪光灯
类方法说明
+ (BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType指定的源类型是否可用,sourceType是枚举类型:
UIImagePickerControllerSourceTypePhotoLibrary:照片库
UIImagePickerControllerSourceTypeCamera:摄像头
UIImagePickerControllerSourceTypeSavedPhotosAlbum:相簿
+ (NSArray *)availableMediaTypesForSourceType:(UIImagePickerControllerSourceType)sourceType指定的源设备上可用的媒体类型,一般就是图片和视频
+ (BOOL)isCameraDeviceAvailable:(UIImagePickerControllerCameraDevice)cameraDevice指定的摄像头是否可用,cameraDevice是枚举类型:
UIImagePickerControllerCameraDeviceRear:前置摄像头
UIImagePickerControllerCameraDeviceFront:后置摄像头
+ (BOOL)isFlashAvailableForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice指定摄像头的闪光灯是否可用
+ (NSArray *)availableCaptureModesForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice获得指定摄像头上的可用捕获模式,捕获模式是枚举类型:
UIImagePickerControllerCameraCaptureModePhoto:拍照模式
UIImagePickerControllerCameraCaptureModeVideo:视频录制模式
对象方法说明
- (void)takePicture编程方式拍照
- (BOOL)startVideoCapture编程方式录制视频
- (void)stopVideoCapture编程方式停止录制视频
代理方法说明
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info媒体拾取完成
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker取消拾取
扩展方法(主要用于保存照片、视频到相簿)说明
UIImageWriteToSavedPhotosAlbum(UIImage *image, id completionTarget, SEL completionSelector, void *contextInfo)保存照片到相簿
UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(NSString *videoPath)能否将视频保存到相簿
void UISaveVideoAtPathToSavedPhotosAlbum(NSString *videoPath, id completionTarget, SEL completionSelector, void *contextInfo)保存视频到相簿

要用UIImagePickerController来拍照或者录制视频通常可以分为如下步骤:

  1. 创建UIImagePickerController对象。
  2. 指定拾取源,平时选择照片时使用的拾取源是照片库或者相簿,此刻需要指定为摄像头类型。
  3. 指定摄像头,前置摄像头或者后置摄像头。
  4. 设置媒体类型mediaType,注意如果是录像必须设置,如果是拍照此步骤可以省略,因为mediaType默认包含kUTTypeImage(注意媒体类型定义在MobileCoreServices.framework中)
  5. 指定捕获模式,拍照或者录制视频。(视频录制时必须先设置媒体类型再设置捕获模式
  6. 展示UIImagePickerController(通常以模态窗口形式打开)。
  7. 拍照和录制视频结束后在代理方法中展示/保存照片或视频。

当然这个过程中有很多细节可以设置,例如是否显示拍照控制面板,拍照后是否允许编辑等等,通过上面的属性/方法列表相信并不难理解。下面就以一个示例展示如何使用UIImagePickerController来拍照和录制视频,下面的程序中只要将_isVideo设置为YES就是视频录制模式,录制完后在主视图控制器中自动播放;如果将_isVideo设置为NO则为拍照模式,拍照完成之后在主视图控制器中显示拍摄的照片:

//
//  ViewController.m
//  UIImagePickerController
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//#import "ViewController.h"
#import <MobileCoreServices/MobileCoreServices.h>
#import <AVFoundation/AVFoundation.h>@interface ViewController ()<UIImagePickerControllerDelegate,UINavigationControllerDelegate>
@property (assign,nonatomic) int isVideo;//是否录制视频,如果为1表示录制视频,0代表拍照
@property (strong,nonatomic) UIImagePickerController *imagePicker;
@property (weak, nonatomic) IBOutlet UIImageView *photo;//照片展示视图
@property (strong ,nonatomic) AVPlayer *player;//播放器,用于录制完视频后播放视频@end@implementation ViewController#pragma mark - 控制器视图事件
- (void)viewDidLoad {[super viewDidLoad];//通过这里设置当前程序是拍照还是录制视频_isVideo=YES;
}#pragma mark - UI事件
//点击拍照按钮
- (IBAction)takeClick:(UIButton *)sender {[self presentViewController:self.imagePicker animated:YES completion:nil];
}#pragma mark - UIImagePickerController代理方法
//完成
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{NSString *mediaType=[info objectForKey:UIImagePickerControllerMediaType];if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {//如果是拍照UIImage *image;//如果允许编辑则获得编辑后的照片,否则获取原始照片if (self.imagePicker.allowsEditing) {image=[info objectForKey:UIImagePickerControllerEditedImage];//获取编辑后的照片}else{image=[info objectForKey:UIImagePickerControllerOriginalImage];//获取原始照片}[self.photo setImage:image];//显示照片UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);//保存到相簿}else if([mediaType isEqualToString:(NSString *)kUTTypeMovie]){//如果是录制视频NSLog(@"video...");NSURL *url=[info objectForKey:UIImagePickerControllerMediaURL];//视频路径NSString *urlStr=[url path];if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(urlStr)) {//保存视频到相簿,注意也可以使用ALAssetsLibrary来保存UISaveVideoAtPathToSavedPhotosAlbum(urlStr, self, @selector(video:didFinishSavingWithError:contextInfo:), nil);//保存视频到相簿}}[self dismissViewControllerAnimated:YES completion:nil];
}
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{NSLog(@"取消");
}#pragma mark - 私有方法
-(UIImagePickerController *)imagePicker{if (!_imagePicker) {_imagePicker=[[UIImagePickerController alloc]init];_imagePicker.sourceType=UIImagePickerControllerSourceTypeCamera;//设置image picker的来源,这里设置为摄像头_imagePicker.cameraDevice=UIImagePickerControllerCameraDeviceRear;//设置使用哪个摄像头,这里设置为后置摄像头if (self.isVideo) {_imagePicker.mediaTypes=@[(NSString *)kUTTypeMovie];_imagePicker.videoQuality=UIImagePickerControllerQualityTypeIFrame1280x720;_imagePicker.cameraCaptureMode=UIImagePickerControllerCameraCaptureModeVideo;//设置摄像头模式(拍照,录制视频)}else{_imagePicker.cameraCaptureMode=UIImagePickerControllerCameraCaptureModePhoto;}_imagePicker.allowsEditing=YES;//允许编辑_imagePicker.delegate=self;//设置代理,检测操作}return _imagePicker;
}//视频保存后的回调
- (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo{if (error) {NSLog(@"保存视频过程中发生错误,错误信息:%@",error.localizedDescription);}else{NSLog(@"视频保存成功.");//录制完之后自动播放NSURL *url=[NSURL fileURLWithPath:videoPath];_player=[AVPlayer playerWithURL:url];AVPlayerLayer *playerLayer=[AVPlayerLayer playerLayerWithPlayer:_player];playerLayer.frame=self.photo.frame;[self.photo.layer addSublayer:playerLayer];[_player play];}
}
@end

运行效果(视频录制):

UIImagePickerController

AVFoundation拍照和录制视频

不得不说UIImagePickerController确实强大,但是与MPMoviePlayerController类似,由于它的高度封装性,要进行某些自定义工作就比较复杂了。例如要做出一款类似于美颜相机的拍照界面就比较难以实现了,此时就可以考虑使用AVFoundation来实现。AVFoundation中提供了很多现成的播放器和录音机,但是事实上它还有更加底层的内容可以供开发者使用。因为AVFoundation中抽了很多和底层输入、输出设备打交道的类,依靠这些类开发人员面对的不再是封装好的音频播放器AVAudioPlayer、录音机(AVAudioRecorder)、视频(包括音频)播放器AVPlayer,而是输入设备(例如麦克风、摄像头)、输出设备(图片、视频)等。首先了解一下使用AVFoundation做拍照和视频录制开发用到的相关类:

AVCaptureSession:媒体(音、视频)捕获会话,负责把捕获的音视频数据输出到输出设备中。一个AVCaptureSession可以有多个输入输出:

AVCaptureSession_InputAndOutput

AVCaptureDevice:输入设备,包括麦克风、摄像头,通过该对象可以设置物理设备的一些属性(例如相机聚焦、白平衡等)。

AVCaptureDeviceInput:设备输入数据管理对象,可以根据AVCaptureDevice创建对应的AVCaptureDeviceInput对象,该对象将会被添加到AVCaptureSession中管理。

AVCaptureOutput:输出数据管理对象,用于接收各类输出数据,通常使用对应的子类AVCaptureAudioDataOutput、AVCaptureStillImageOutput、AVCaptureVideoDataOutput、AVCaptureFileOutput,该对象将会被添加到AVCaptureSession中管理。注意:前面几个对象的输出数据都是NSData类型,而AVCaptureFileOutput代表数据以文件形式输出,类似的,AVCcaptureFileOutput也不会直接创建使用,通常会使用其子类:AVCaptureAudioFileOutput、AVCaptureMovieFileOutput。当把一个输入或者输出添加到AVCaptureSession之后AVCaptureSession就会在所有相符的输入、输出设备之间建立连接(AVCaptionConnection):

AVCaptureSession_Relation

AVCaptureVideoPreviewLayer:相机拍摄预览图层,是CALayer的子类,使用该对象可以实时查看拍照或视频录制效果,创建该对象需要指定对应的AVCaptureSession对象。

使用AVFoundation拍照和录制视频的一般步骤如下:

  1. 创建AVCaptureSession对象。
  2. 使用AVCaptureDevice的静态方法获得需要使用的设备,例如拍照和录像就需要获得摄像头设备,录音就要获得麦克风设备。
  3. 利用输入设备AVCaptureDevice初始化AVCaptureDeviceInput对象。
  4. 初始化输出数据管理对象,如果要拍照就初始化AVCaptureStillImageOutput对象;如果拍摄视频就初始化AVCaptureMovieFileOutput对象。
  5. 将数据输入对象AVCaptureDeviceInput、数据输出对象AVCaptureOutput添加到媒体会话管理对象AVCaptureSession中。
  6. 创建视频预览图层AVCaptureVideoPreviewLayer并指定媒体会话,添加图层到显示容器中,调用AVCaptureSession的startRuning方法开始捕获。
  7. 将捕获的音频或视频数据输出到指定文件。

拍照

下面看一下如何使用AVFoundation实现一个拍照程序,在这个程序中将实现摄像头预览、切换前后摄像头、闪光灯设置、对焦、拍照保存等功能。应用大致效果如下:

AVFoundation_CameraRunEffect

在程序中定义会话、输入、输出等相关对象。

@interface ViewController ()
@property (strong,nonatomic) AVCaptureSession *captureSession;//负责输入和输出设备之间的数据传递
@property (strong,nonatomic) AVCaptureDeviceInput *captureDeviceInput;//负责从AVCaptureDevice获得输入数据
@property (strong,nonatomic) AVCaptureStillImageOutput *captureStillImageOutput;//照片输出流
@property (strong,nonatomic) AVCaptureVideoPreviewLayer *captureVideoPreviewLayer;//相机拍摄预览图层
@property (weak, nonatomic) IBOutlet UIView *viewContainer;
@property (weak, nonatomic) IBOutlet UIButton *takeButton;//拍照按钮
@property (weak, nonatomic) IBOutlet UIButton *flashAutoButton;//自动闪光灯按钮
@property (weak, nonatomic) IBOutlet UIButton *flashOnButton;//打开闪光灯按钮
@property (weak, nonatomic) IBOutlet UIButton *flashOffButton;//关闭闪光灯按钮
@property (weak, nonatomic) IBOutlet UIImageView *focusCursor; //聚焦光标
@end

在控制器视图将要展示时创建并初始化会话、摄像头设备、输入、输出、预览图层,并且添加预览图层到视图中,除此之外还做了一些初始化工作,例如添加手势(点击屏幕进行聚焦)、初始化界面等。

-(void)viewWillAppear:(BOOL)animated{[super viewWillAppear:animated];//初始化会话_captureSession=[[AVCaptureSession alloc]init];if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {//设置分辨率_captureSession.sessionPreset=AVCaptureSessionPreset1280x720;}//获得输入设备AVCaptureDevice *captureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];//取得后置摄像头if (!captureDevice) {NSLog(@"取得后置摄像头时出现问题.");return;}NSError *error=nil;//根据输入设备初始化设备输入对象,用于获得输入数据_captureDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:captureDevice error:&error];if (error) {NSLog(@"取得设备输入对象时出错,错误原因:%@",error.localizedDescription);return;}//初始化设备输出对象,用于获得输出数据_captureStillImageOutput=[[AVCaptureStillImageOutput alloc]init];NSDictionary *outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};[_captureStillImageOutput setOutputSettings:outputSettings];//输出设置//将设备输入添加到会话中if ([_captureSession canAddInput:_captureDeviceInput]) {[_captureSession addInput:_captureDeviceInput];}//将设备输出添加到会话中if ([_captureSession canAddOutput:_captureStillImageOutput]) {[_captureSession addOutput:_captureStillImageOutput];}//创建视频预览层,用于实时展示摄像头状态_captureVideoPreviewLayer=[[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession];CALayer *layer=self.viewContainer.layer;layer.masksToBounds=YES;_captureVideoPreviewLayer.frame=layer.bounds;_captureVideoPreviewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式//将视频预览层添加到界面中//[layer addSublayer:_captureVideoPreviewLayer];[layer insertSublayer:_captureVideoPreviewLayer below:self.focusCursor.layer];[self addNotificationToCaptureDevice:captureDevice];[self addGenstureRecognizer];[self setFlashModeButtonStatus];
}

在控制器视图展示和视图离开界面时启动、停止会话。

-(void)viewDidAppear:(BOOL)animated{[super viewDidAppear:animated];[self.captureSession startRunning];
}-(void)viewDidDisappear:(BOOL)animated{[super viewDidDisappear:animated];[self.captureSession stopRunning];
}

定义闪光灯开闭及自动模式功能,注意无论是设置闪光灯、白平衡还是其他输入设备属性,在设置之前必须先锁定配置,修改完后解锁。

/***  改变设备属性的统一操作方法**  @param propertyChange 属性改变操作*/
-(void)changeDeviceProperty:(PropertyChangeBlock)propertyChange{AVCaptureDevice *captureDevice= [self.captureDeviceInput device];NSError *error;//注意改变设备属性前一定要首先调用lockForConfiguration:调用完之后使用unlockForConfiguration方法解锁if ([captureDevice lockForConfiguration:&error]) {propertyChange(captureDevice);[captureDevice unlockForConfiguration];}else{NSLog(@"设置设备属性过程发生错误,错误信息:%@",error.localizedDescription);}
}/***  设置闪光灯模式**  @param flashMode 闪光灯模式*/
-(void)setFlashMode:(AVCaptureFlashMode )flashMode{[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {if ([captureDevice isFlashModeSupported:flashMode]) {[captureDevice setFlashMode:flashMode];}}];
}

定义切换摄像头功能,切换摄像头的过程就是将原有输入移除,在会话中添加新的输入,但是注意动态修改会话需要首先开启配置,配置成功后提交配置。

#pragma mark 切换前后摄像头
- (IBAction)toggleButtonClick:(UIButton *)sender {AVCaptureDevice *currentDevice=[self.captureDeviceInput device];AVCaptureDevicePosition currentPosition=[currentDevice position];[self removeNotificationFromCaptureDevice:currentDevice];AVCaptureDevice *toChangeDevice;AVCaptureDevicePosition toChangePosition=AVCaptureDevicePositionFront;if (currentPosition==AVCaptureDevicePositionUnspecified||currentPosition==AVCaptureDevicePositionFront) {toChangePosition=AVCaptureDevicePositionBack;}toChangeDevice=[self getCameraDeviceWithPosition:toChangePosition];[self addNotificationToCaptureDevice:toChangeDevice];//获得要调整的设备输入对象AVCaptureDeviceInput *toChangeDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:toChangeDevice error:nil];//改变会话的配置前一定要先开启配置,配置完成后提交配置改变[self.captureSession beginConfiguration];//移除原有输入对象[self.captureSession removeInput:self.captureDeviceInput];//添加新的输入对象if ([self.captureSession canAddInput:toChangeDeviceInput]) {[self.captureSession addInput:toChangeDeviceInput];self.captureDeviceInput=toChangeDeviceInput;}//提交会话配置[self.captureSession commitConfiguration];[self setFlashModeButtonStatus];
}

添加点击手势操作,点按预览视图时进行聚焦、白平衡设置。

/***  设置聚焦点**  @param point 聚焦点*/
-(void)focusWithMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode atPoint:(CGPoint)point{[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {if ([captureDevice isFocusModeSupported:focusMode]) {[captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];}if ([captureDevice isFocusPointOfInterestSupported]) {[captureDevice setFocusPointOfInterest:point];}if ([captureDevice isExposureModeSupported:exposureMode]) {[captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];}if ([captureDevice isExposurePointOfInterestSupported]) {[captureDevice setExposurePointOfInterest:point];}}];
}/***  添加点按手势,点按时聚焦*/
-(void)addGenstureRecognizer{UITapGestureRecognizer *tapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapScreen:)];[self.viewContainer addGestureRecognizer:tapGesture];
}
-(void)tapScreen:(UITapGestureRecognizer *)tapGesture{CGPoint point= [tapGesture locationInView:self.viewContainer];//将UI坐标转化为摄像头坐标CGPoint cameraPoint= [self.captureVideoPreviewLayer captureDevicePointOfInterestForPoint:point];[self setFocusCursorWithPoint:point];[self focusWithMode:AVCaptureFocusModeAutoFocus exposureMode:AVCaptureExposureModeAutoExpose atPoint:cameraPoint];
}

定义拍照功能,拍照的过程就是获取连接,从连接中获得捕获的输出数据并做保存操作。

#pragma mark 拍照
- (IBAction)takeButtonClick:(UIButton *)sender {//根据设备输出获得连接AVCaptureConnection *captureConnection=[self.captureStillImageOutput connectionWithMediaType:AVMediaTypeVideo];//根据连接取得设备输出的数据[self.captureStillImageOutput captureStillImageAsynchronouslyFromConnection:captureConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {if (imageDataSampleBuffer) {NSData *imageData=[AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];UIImage *image=[UIImage imageWithData:imageData];UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
//            ALAssetsLibrary *assetsLibrary=[[ALAssetsLibrary alloc]init];
//            [assetsLibrary writeImageToSavedPhotosAlbum:[image CGImage] orientation:(ALAssetOrientation)[image imageOrientation] completionBlock:nil];}}];
}

最后附上完整代码:

//
//  ViewController.m
//  AVFoundationCamera
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#import <AssetsLibrary/AssetsLibrary.h>
typedef void(^PropertyChangeBlock)(AVCaptureDevice *captureDevice);@interface ViewController ()@property (strong,nonatomic) AVCaptureSession *captureSession;//负责输入和输出设备之间的数据传递
@property (strong,nonatomic) AVCaptureDeviceInput *captureDeviceInput;//负责从AVCaptureDevice获得输入数据
@property (strong,nonatomic) AVCaptureStillImageOutput *captureStillImageOutput;//照片输出流
@property (strong,nonatomic) AVCaptureVideoPreviewLayer *captureVideoPreviewLayer;//相机拍摄预览图层
@property (weak, nonatomic) IBOutlet UIView *viewContainer;
@property (weak, nonatomic) IBOutlet UIButton *takeButton;//拍照按钮
@property (weak, nonatomic) IBOutlet UIButton *flashAutoButton;//自动闪光灯按钮
@property (weak, nonatomic) IBOutlet UIButton *flashOnButton;//打开闪光灯按钮
@property (weak, nonatomic) IBOutlet UIButton *flashOffButton;//关闭闪光灯按钮
@property (weak, nonatomic) IBOutlet UIImageView *focusCursor; //聚焦光标@end@implementation ViewController#pragma mark - 控制器视图方法
- (void)viewDidLoad {[super viewDidLoad];}-(void)viewWillAppear:(BOOL)animated{[super viewWillAppear:animated];//初始化会话_captureSession=[[AVCaptureSession alloc]init];if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {//设置分辨率_captureSession.sessionPreset=AVCaptureSessionPreset1280x720;}//获得输入设备AVCaptureDevice *captureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];//取得后置摄像头if (!captureDevice) {NSLog(@"取得后置摄像头时出现问题.");return;}NSError *error=nil;//根据输入设备初始化设备输入对象,用于获得输入数据_captureDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:captureDevice error:&error];if (error) {NSLog(@"取得设备输入对象时出错,错误原因:%@",error.localizedDescription);return;}//初始化设备输出对象,用于获得输出数据_captureStillImageOutput=[[AVCaptureStillImageOutput alloc]init];NSDictionary *outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};[_captureStillImageOutput setOutputSettings:outputSettings];//输出设置//将设备输入添加到会话中if ([_captureSession canAddInput:_captureDeviceInput]) {[_captureSession addInput:_captureDeviceInput];}//将设备输出添加到会话中if ([_captureSession canAddOutput:_captureStillImageOutput]) {[_captureSession addOutput:_captureStillImageOutput];}//创建视频预览层,用于实时展示摄像头状态_captureVideoPreviewLayer=[[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession];CALayer *layer=self.viewContainer.layer;layer.masksToBounds=YES;_captureVideoPreviewLayer.frame=layer.bounds;_captureVideoPreviewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式//将视频预览层添加到界面中//[layer addSublayer:_captureVideoPreviewLayer];[layer insertSublayer:_captureVideoPreviewLayer below:self.focusCursor.layer];[self addNotificationToCaptureDevice:captureDevice];[self addGenstureRecognizer];[self setFlashModeButtonStatus];
}-(void)viewDidAppear:(BOOL)animated{[super viewDidAppear:animated];[self.captureSession startRunning];
}-(void)viewDidDisappear:(BOOL)animated{[super viewDidDisappear:animated];[self.captureSession stopRunning];
}-(void)dealloc{[self removeNotification];
}
#pragma mark - UI方法
#pragma mark 拍照
- (IBAction)takeButtonClick:(UIButton *)sender {//根据设备输出获得连接AVCaptureConnection *captureConnection=[self.captureStillImageOutput connectionWithMediaType:AVMediaTypeVideo];//根据连接取得设备输出的数据[self.captureStillImageOutput captureStillImageAsynchronouslyFromConnection:captureConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {if (imageDataSampleBuffer) {NSData *imageData=[AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];UIImage *image=[UIImage imageWithData:imageData];UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
//            ALAssetsLibrary *assetsLibrary=[[ALAssetsLibrary alloc]init];
//            [assetsLibrary writeImageToSavedPhotosAlbum:[image CGImage] orientation:(ALAssetOrientation)[image imageOrientation] completionBlock:nil];}}];
}
#pragma mark 切换前后摄像头
- (IBAction)toggleButtonClick:(UIButton *)sender {AVCaptureDevice *currentDevice=[self.captureDeviceInput device];AVCaptureDevicePosition currentPosition=[currentDevice position];[self removeNotificationFromCaptureDevice:currentDevice];AVCaptureDevice *toChangeDevice;AVCaptureDevicePosition toChangePosition=AVCaptureDevicePositionFront;if (currentPosition==AVCaptureDevicePositionUnspecified||currentPosition==AVCaptureDevicePositionFront) {toChangePosition=AVCaptureDevicePositionBack;}toChangeDevice=[self getCameraDeviceWithPosition:toChangePosition];[self addNotificationToCaptureDevice:toChangeDevice];//获得要调整的设备输入对象AVCaptureDeviceInput *toChangeDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:toChangeDevice error:nil];//改变会话的配置前一定要先开启配置,配置完成后提交配置改变[self.captureSession beginConfiguration];//移除原有输入对象[self.captureSession removeInput:self.captureDeviceInput];//添加新的输入对象if ([self.captureSession canAddInput:toChangeDeviceInput]) {[self.captureSession addInput:toChangeDeviceInput];self.captureDeviceInput=toChangeDeviceInput;}//提交会话配置[self.captureSession commitConfiguration];[self setFlashModeButtonStatus];
}#pragma mark 自动闪光灯开启
- (IBAction)flashAutoClick:(UIButton *)sender {[self setFlashMode:AVCaptureFlashModeAuto];[self setFlashModeButtonStatus];
}
#pragma mark 打开闪光灯
- (IBAction)flashOnClick:(UIButton *)sender {[self setFlashMode:AVCaptureFlashModeOn];[self setFlashModeButtonStatus];
}
#pragma mark 关闭闪光灯
- (IBAction)flashOffClick:(UIButton *)sender {[self setFlashMode:AVCaptureFlashModeOff];[self setFlashModeButtonStatus];
}#pragma mark - 通知
/***  给输入设备添加通知*/
-(void)addNotificationToCaptureDevice:(AVCaptureDevice *)captureDevice{//注意添加区域改变捕获通知必须首先设置设备允许捕获[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {captureDevice.subjectAreaChangeMonitoringEnabled=YES;}];NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];//捕获区域发生改变[notificationCenter addObserver:self selector:@selector(areaChange:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice];
}
-(void)removeNotificationFromCaptureDevice:(AVCaptureDevice *)captureDevice{NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];[notificationCenter removeObserver:self name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice];
}
/***  移除所有通知*/
-(void)removeNotification{NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];[notificationCenter removeObserver:self];
}-(void)addNotificationToCaptureSession:(AVCaptureSession *)captureSession{NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];//会话出错[notificationCenter addObserver:self selector:@selector(sessionRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:captureSession];
}/***  设备连接成功**  @param notification 通知对象*/
-(void)deviceConnected:(NSNotification *)notification{NSLog(@"设备已连接...");
}
/***  设备连接断开**  @param notification 通知对象*/
-(void)deviceDisconnected:(NSNotification *)notification{NSLog(@"设备已断开.");
}
/***  捕获区域改变**  @param notification 通知对象*/
-(void)areaChange:(NSNotification *)notification{NSLog(@"捕获区域改变...");
}/***  会话出错**  @param notification 通知对象*/
-(void)sessionRuntimeError:(NSNotification *)notification{NSLog(@"会话发生错误.");
}#pragma mark - 私有方法/***  取得指定位置的摄像头**  @param position 摄像头位置**  @return 摄像头设备*/
-(AVCaptureDevice *)getCameraDeviceWithPosition:(AVCaptureDevicePosition )position{NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];for (AVCaptureDevice *camera in cameras) {if ([camera position]==position) {return camera;}}return nil;
}/***  改变设备属性的统一操作方法**  @param propertyChange 属性改变操作*/
-(void)changeDeviceProperty:(PropertyChangeBlock)propertyChange{AVCaptureDevice *captureDevice= [self.captureDeviceInput device];NSError *error;//注意改变设备属性前一定要首先调用lockForConfiguration:调用完之后使用unlockForConfiguration方法解锁if ([captureDevice lockForConfiguration:&error]) {propertyChange(captureDevice);[captureDevice unlockForConfiguration];}else{NSLog(@"设置设备属性过程发生错误,错误信息:%@",error.localizedDescription);}
}/***  设置闪光灯模式**  @param flashMode 闪光灯模式*/
-(void)setFlashMode:(AVCaptureFlashMode )flashMode{[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {if ([captureDevice isFlashModeSupported:flashMode]) {[captureDevice setFlashMode:flashMode];}}];
}
/***  设置聚焦模式**  @param focusMode 聚焦模式*/
-(void)setFocusMode:(AVCaptureFocusMode )focusMode{[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {if ([captureDevice isFocusModeSupported:focusMode]) {[captureDevice setFocusMode:focusMode];}}];
}
/***  设置曝光模式**  @param exposureMode 曝光模式*/
-(void)setExposureMode:(AVCaptureExposureMode)exposureMode{[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {if ([captureDevice isExposureModeSupported:exposureMode]) {[captureDevice setExposureMode:exposureMode];}}];
}
/***  设置聚焦点**  @param point 聚焦点*/
-(void)focusWithMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode atPoint:(CGPoint)point{[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {if ([captureDevice isFocusModeSupported:focusMode]) {[captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];}if ([captureDevice isFocusPointOfInterestSupported]) {[captureDevice setFocusPointOfInterest:point];}if ([captureDevice isExposureModeSupported:exposureMode]) {[captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];}if ([captureDevice isExposurePointOfInterestSupported]) {[captureDevice setExposurePointOfInterest:point];}}];
}/***  添加点按手势,点按时聚焦*/
-(void)addGenstureRecognizer{UITapGestureRecognizer *tapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapScreen:)];[self.viewContainer addGestureRecognizer:tapGesture];
}
-(void)tapScreen:(UITapGestureRecognizer *)tapGesture{CGPoint point= [tapGesture locationInView:self.viewContainer];//将UI坐标转化为摄像头坐标CGPoint cameraPoint= [self.captureVideoPreviewLayer captureDevicePointOfInterestForPoint:point];[self setFocusCursorWithPoint:point];[self focusWithMode:AVCaptureFocusModeAutoFocus exposureMode:AVCaptureExposureModeAutoExpose atPoint:cameraPoint];
}/***  设置闪光灯按钮状态*/
-(void)setFlashModeButtonStatus{AVCaptureDevice *captureDevice=[self.captureDeviceInput device];AVCaptureFlashMode flashMode=captureDevice.flashMode;if([captureDevice isFlashAvailable]){self.flashAutoButton.hidden=NO;self.flashOnButton.hidden=NO;self.flashOffButton.hidden=NO;self.flashAutoButton.enabled=YES;self.flashOnButton.enabled=YES;self.flashOffButton.enabled=YES;switch (flashMode) {case AVCaptureFlashModeAuto:self.flashAutoButton.enabled=NO;break;case AVCaptureFlashModeOn:self.flashOnButton.enabled=NO;break;case AVCaptureFlashModeOff:self.flashOffButton.enabled=NO;break;default:break;}}else{self.flashAutoButton.hidden=YES;self.flashOnButton.hidden=YES;self.flashOffButton.hidden=YES;}
}/***  设置聚焦光标位置**  @param point 光标位置*/
-(void)setFocusCursorWithPoint:(CGPoint)point{self.focusCursor.center=point;self.focusCursor.transform=CGAffineTransformMakeScale(1.5, 1.5);self.focusCursor.alpha=1.0;[UIView animateWithDuration:1.0 animations:^{self.focusCursor.transform=CGAffineTransformIdentity;} completion:^(BOOL finished) {self.focusCursor.alpha=0;}];
}
@end

运行效果:

视频录制

其实有了前面的拍照应用之后要在此基础上做视频录制功能并不复杂,程序只需要做如下修改:

  1. 添加一个音频输入到会话(使用[[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject]获得输入设备,然后根据此输入设备创建一个设备输入对象),在拍照程序中已经添加了视频输入所以此时不需要添加视频输入。
  2. 创建一个音乐播放文件输出对象AVCaptureMovieFileOutput取代原来的照片输出对象。
  3. 将捕获到的视频数据写入到临时文件并在停止录制之后保存到相簿(通过AVCaptureMovieFileOutput的代理方法)。

相比拍照程序,程序的修改主要就是以上三点。当然为了让程序更加完善在下面的视频录制程序中加入了屏幕旋转视频、自动布局和后台保存任务等细节。下面是修改后的程序:

//
//  ViewController.m
//  AVFoundationCamera
//
//  Created by Kenshin Cui on 14/04/05.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//  视频录制#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#import <AssetsLibrary/AssetsLibrary.h>
typedef void(^PropertyChangeBlock)(AVCaptureDevice *captureDevice);@interface ViewController ()<AVCaptureFileOutputRecordingDelegate>//视频文件输出代理@property (strong,nonatomic) AVCaptureSession *captureSession;//负责输入和输出设备之间的数据传递
@property (strong,nonatomic) AVCaptureDeviceInput *captureDeviceInput;//负责从AVCaptureDevice获得输入数据
@property (strong,nonatomic) AVCaptureMovieFileOutput *captureMovieFileOutput;//视频输出流
@property (strong,nonatomic) AVCaptureVideoPreviewLayer *captureVideoPreviewLayer;//相机拍摄预览图层
@property (assign,nonatomic) BOOL enableRotation;//是否允许旋转(注意在视频录制过程中禁止屏幕旋转)
@property (assign,nonatomic) CGRect *lastBounds;//旋转的前大小
@property (assign,nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;//后台任务标识
@property (weak, nonatomic) IBOutlet UIView *viewContainer;
@property (weak, nonatomic) IBOutlet UIButton *takeButton;//拍照按钮
@property (weak, nonatomic) IBOutlet UIImageView *focusCursor; //聚焦光标@end@implementation ViewController#pragma mark - 控制器视图方法
- (void)viewDidLoad {[super viewDidLoad];
}-(void)viewWillAppear:(BOOL)animated{[super viewWillAppear:animated];//初始化会话_captureSession=[[AVCaptureSession alloc]init];if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {//设置分辨率_captureSession.sessionPreset=AVCaptureSessionPreset1280x720;}//获得输入设备AVCaptureDevice *captureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];//取得后置摄像头if (!captureDevice) {NSLog(@"取得后置摄像头时出现问题.");return;}//添加一个音频输入设备AVCaptureDevice *audioCaptureDevice=[[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject];NSError *error=nil;//根据输入设备初始化设备输入对象,用于获得输入数据_captureDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:captureDevice error:&error];if (error) {NSLog(@"取得设备输入对象时出错,错误原因:%@",error.localizedDescription);return;}AVCaptureDeviceInput *audioCaptureDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:audioCaptureDevice error:&error];if (error) {NSLog(@"取得设备输入对象时出错,错误原因:%@",error.localizedDescription);return;}//初始化设备输出对象,用于获得输出数据_captureMovieFileOutput=[[AVCaptureMovieFileOutput alloc]init];//将设备输入添加到会话中if ([_captureSession canAddInput:_captureDeviceInput]) {[_captureSession addInput:_captureDeviceInput];[_captureSession addInput:audioCaptureDeviceInput];AVCaptureConnection *captureConnection=[_captureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];if ([captureConnection isVideoStabilizationSupported ]) {captureConnection.preferredVideoStabilizationMode=AVCaptureVideoStabilizationModeAuto;}}//将设备输出添加到会话中if ([_captureSession canAddOutput:_captureMovieFileOutput]) {[_captureSession addOutput:_captureMovieFileOutput];}//创建视频预览层,用于实时展示摄像头状态_captureVideoPreviewLayer=[[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession];CALayer *layer=self.viewContainer.layer;layer.masksToBounds=YES;_captureVideoPreviewLayer.frame=layer.bounds;_captureVideoPreviewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式//将视频预览层添加到界面中//[layer addSublayer:_captureVideoPreviewLayer];[layer insertSublayer:_captureVideoPreviewLayer below:self.focusCursor.layer];_enableRotation=YES;[self addNotificationToCaptureDevice:captureDevice];[self addGenstureRecognizer];
}-(void)viewDidAppear:(BOOL)animated{[super viewDidAppear:animated];[self.captureSession startRunning];
}-(void)viewDidDisappear:(BOOL)animated{[super viewDidDisappear:animated];[self.captureSession stopRunning];
}-(BOOL)shouldAutorotate{return self.enableRotation;
}屏幕旋转时调整视频预览图层的方向
//-(void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator{
//    [super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];NSLog(@"%i,%i",newCollection.verticalSizeClass,newCollection.horizontalSizeClass);
//    UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
//    NSLog(@"%i",orientation);
//    AVCaptureConnection *captureConnection=[self.captureVideoPreviewLayer connection];
//    captureConnection.videoOrientation=orientation;
//    
//}
//屏幕旋转时调整视频预览图层的方向
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration{AVCaptureConnection *captureConnection=[self.captureVideoPreviewLayer connection];captureConnection.videoOrientation=(AVCaptureVideoOrientation)toInterfaceOrientation;
}
//旋转后重新设置大小
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{_captureVideoPreviewLayer.frame=self.viewContainer.bounds;
}-(void)dealloc{[self removeNotification];
}
#pragma mark - UI方法
#pragma mark 视频录制
- (IBAction)takeButtonClick:(UIButton *)sender {//根据设备输出获得连接AVCaptureConnection *captureConnection=[self.captureMovieFileOutput connectionWithMediaType:AVMediaTypeVideo];//根据连接取得设备输出的数据if (![self.captureMovieFileOutput isRecording]) {self.enableRotation=NO;//如果支持多任务则则开始多任务if ([[UIDevice currentDevice] isMultitaskingSupported]) {self.backgroundTaskIdentifier=[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];}//预览图层和视频方向保持一致captureConnection.videoOrientation=[self.captureVideoPreviewLayer connection].videoOrientation;NSString *outputFielPath=[NSTemporaryDirectory() stringByAppendingString:@"myMovie.mov"];NSLog(@"save path is :%@",outputFielPath);NSURL *fileUrl=[NSURL fileURLWithPath:outputFielPath];[self.captureMovieFileOutput startRecordingToOutputFileURL:fileUrl recordingDelegate:self];}else{[self.captureMovieFileOutput stopRecording];//停止录制}
}
#pragma mark 切换前后摄像头
- (IBAction)toggleButtonClick:(UIButton *)sender {AVCaptureDevice *currentDevice=[self.captureDeviceInput device];AVCaptureDevicePosition currentPosition=[currentDevice position];[self removeNotificationFromCaptureDevice:currentDevice];AVCaptureDevice *toChangeDevice;AVCaptureDevicePosition toChangePosition=AVCaptureDevicePositionFront;if (currentPosition==AVCaptureDevicePositionUnspecified||currentPosition==AVCaptureDevicePositionFront) {toChangePosition=AVCaptureDevicePositionBack;}toChangeDevice=[self getCameraDeviceWithPosition:toChangePosition];[self addNotificationToCaptureDevice:toChangeDevice];//获得要调整的设备输入对象AVCaptureDeviceInput *toChangeDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:toChangeDevice error:nil];//改变会话的配置前一定要先开启配置,配置完成后提交配置改变[self.captureSession beginConfiguration];//移除原有输入对象[self.captureSession removeInput:self.captureDeviceInput];//添加新的输入对象if ([self.captureSession canAddInput:toChangeDeviceInput]) {[self.captureSession addInput:toChangeDeviceInput];self.captureDeviceInput=toChangeDeviceInput;}//提交会话配置[self.captureSession commitConfiguration];}#pragma mark - 视频输出代理
-(void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections{NSLog(@"开始录制...");
}
-(void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error{NSLog(@"视频录制完成.");//视频录入完成之后在后台将视频存储到相簿self.enableRotation=YES;UIBackgroundTaskIdentifier lastBackgroundTaskIdentifier=self.backgroundTaskIdentifier;self.backgroundTaskIdentifier=UIBackgroundTaskInvalid;ALAssetsLibrary *assetsLibrary=[[ALAssetsLibrary alloc]init];[assetsLibrary writeVideoAtPathToSavedPhotosAlbum:outputFileURL completionBlock:^(NSURL *assetURL, NSError *error) {if (error) {NSLog(@"保存视频到相簿过程中发生错误,错误信息:%@",error.localizedDescription);}if (lastBackgroundTaskIdentifier!=UIBackgroundTaskInvalid) {[[UIApplication sharedApplication] endBackgroundTask:lastBackgroundTaskIdentifier];}NSLog(@"成功保存视频到相簿.");}];}#pragma mark - 通知
/***  给输入设备添加通知*/
-(void)addNotificationToCaptureDevice:(AVCaptureDevice *)captureDevice{//注意添加区域改变捕获通知必须首先设置设备允许捕获[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {captureDevice.subjectAreaChangeMonitoringEnabled=YES;}];NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];//捕获区域发生改变[notificationCenter addObserver:self selector:@selector(areaChange:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice];
}
-(void)removeNotificationFromCaptureDevice:(AVCaptureDevice *)captureDevice{NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];[notificationCenter removeObserver:self name:AVCaptureDeviceSubjectAreaDidChangeNotification object:captureDevice];
}
/***  移除所有通知*/
-(void)removeNotification{NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];[notificationCenter removeObserver:self];
}-(void)addNotificationToCaptureSession:(AVCaptureSession *)captureSession{NSNotificationCenter *notificationCenter= [NSNotificationCenter defaultCenter];//会话出错[notificationCenter addObserver:self selector:@selector(sessionRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:captureSession];
}/***  设备连接成功**  @param notification 通知对象*/
-(void)deviceConnected:(NSNotification *)notification{NSLog(@"设备已连接...");
}
/***  设备连接断开**  @param notification 通知对象*/
-(void)deviceDisconnected:(NSNotification *)notification{NSLog(@"设备已断开.");
}
/***  捕获区域改变**  @param notification 通知对象*/
-(void)areaChange:(NSNotification *)notification{NSLog(@"捕获区域改变...");
}/***  会话出错**  @param notification 通知对象*/
-(void)sessionRuntimeError:(NSNotification *)notification{NSLog(@"会话发生错误.");
}#pragma mark - 私有方法/***  取得指定位置的摄像头**  @param position 摄像头位置**  @return 摄像头设备*/
-(AVCaptureDevice *)getCameraDeviceWithPosition:(AVCaptureDevicePosition )position{NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];for (AVCaptureDevice *camera in cameras) {if ([camera position]==position) {return camera;}}return nil;
}/***  改变设备属性的统一操作方法**  @param propertyChange 属性改变操作*/
-(void)changeDeviceProperty:(PropertyChangeBlock)propertyChange{AVCaptureDevice *captureDevice= [self.captureDeviceInput device];NSError *error;//注意改变设备属性前一定要首先调用lockForConfiguration:调用完之后使用unlockForConfiguration方法解锁if ([captureDevice lockForConfiguration:&error]) {propertyChange(captureDevice);[captureDevice unlockForConfiguration];}else{NSLog(@"设置设备属性过程发生错误,错误信息:%@",error.localizedDescription);}
}/***  设置闪光灯模式**  @param flashMode 闪光灯模式*/
-(void)setFlashMode:(AVCaptureFlashMode )flashMode{[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {if ([captureDevice isFlashModeSupported:flashMode]) {[captureDevice setFlashMode:flashMode];}}];
}
/***  设置聚焦模式**  @param focusMode 聚焦模式*/
-(void)setFocusMode:(AVCaptureFocusMode )focusMode{[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {if ([captureDevice isFocusModeSupported:focusMode]) {[captureDevice setFocusMode:focusMode];}}];
}
/***  设置曝光模式**  @param exposureMode 曝光模式*/
-(void)setExposureMode:(AVCaptureExposureMode)exposureMode{[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {if ([captureDevice isExposureModeSupported:exposureMode]) {[captureDevice setExposureMode:exposureMode];}}];
}
/***  设置聚焦点**  @param point 聚焦点*/
-(void)focusWithMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode atPoint:(CGPoint)point{[self changeDeviceProperty:^(AVCaptureDevice *captureDevice) {if ([captureDevice isFocusModeSupported:focusMode]) {[captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];}if ([captureDevice isFocusPointOfInterestSupported]) {[captureDevice setFocusPointOfInterest:point];}if ([captureDevice isExposureModeSupported:exposureMode]) {[captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];}if ([captureDevice isExposurePointOfInterestSupported]) {[captureDevice setExposurePointOfInterest:point];}}];
}/***  添加点按手势,点按时聚焦*/
-(void)addGenstureRecognizer{UITapGestureRecognizer *tapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapScreen:)];[self.viewContainer addGestureRecognizer:tapGesture];
}
-(void)tapScreen:(UITapGestureRecognizer *)tapGesture{CGPoint point= [tapGesture locationInView:self.viewContainer];//将UI坐标转化为摄像头坐标CGPoint cameraPoint= [self.captureVideoPreviewLayer captureDevicePointOfInterestForPoint:point];[self setFocusCursorWithPoint:point];[self focusWithMode:AVCaptureFocusModeAutoFocus exposureMode:AVCaptureExposureModeAutoExpose atPoint:cameraPoint];
}/***  设置聚焦光标位置**  @param point 光标位置*/
-(void)setFocusCursorWithPoint:(CGPoint)point{self.focusCursor.center=point;self.focusCursor.transform=CGAffineTransformMakeScale(1.5, 1.5);self.focusCursor.alpha=1.0;[UIView animateWithDuration:1.0 animations:^{self.focusCursor.transform=CGAffineTransformIdentity;} completion:^(BOOL finished) {self.focusCursor.alpha=0;}];
}
@end

运行效果:

 

总结

前面用了大量的篇幅介绍了iOS中的音、视频播放和录制,有些地方用到了封装好的播放器、录音机直接使用,有些是直接调用系统服务自己组织封装,正如本篇开头所言,iOS对于多媒体支持相当灵活和完善,那么开放过程中如何选择呢,下面就以一个表格简单对比一下各个开发技术的优缺点。

summary

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 高级程序员的进阶之路-底层知识篇

    高级程序员的进阶之路-底层知识篇 今天在和同事闲聊的时候,谈论到一个高级程序员应该具备什么的能力,其实这种问题都是老生畅谈的问题了,最主要的还是基础知识的牢固和算法知识的积累,有些工作了好多年了,没有自己的知识积累库,今天我就给大家分享几个我常学习的和原来看…...

    2024/5/7 1:13:48
  2. React性能优化小贴士

    React性能优化小贴士...

    2024/5/7 14:43:10
  3. 2020中国华录杯邀请码:423809

    2020中国华录杯报名地址:https://segmentfault.com/e/1160000023520111...

    2024/5/6 23:21:57
  4. 小程序 实现老虎机抽奖 - 可以点击N次,试用各种机型,不会产生误差

    先看效果图: 点击抽奖,就滚动到后端返回对应的数字,试用各种机型之前试用了background-position的方式去改变数字的背景位置,发奖在小程序使用rpx单位的情况下,会产生误差,且不适配各种机型,于是经过大佬的请教下,改成使用transform: rotateX的方试,话不多说,先上代码…...

    2024/5/7 18:32:21
  5. Linux-入门(7.网络配置)学习笔记

    文章目录1. 查看网络IP和网关2. ping 测试主机之间网络连通性3. 虚拟机上 Linux 上网简单图解4. linux网络环境配置1. 第一种方法(自动获取)2. 第二种方法(指定固定的ip) 1. 查看网络IP和网关查看虚拟网络编辑器修改ip地址查看网关查看 Linux 环境的中VMnet8网络配置 指令 : i…...

    2024/4/17 16:01:59
  6. 阿帆谈币:8.28以太坊区间震荡 日内行情修复 是准备在冲牛市吗?

    各位朋友们,你们好, 我是CKcoin平台分析师阿帆谈币。阿帆本着负责、诚恳、认真的态度用心写好每一篇分析文章,特点鲜明,不夸张,不含糊,力求能让大家看懂大的趋势分析以及小范围的多空搏杀力度!技术面4小时看以太昨晚触及391后尝试破位下轨被强烈支撑,日内行情开始倾斜缓…...

    2024/5/4 5:55:01
  7. gitlab_2020-08-28

    目标:使用168.61.113.220作为新服务器,把gitlab 8.10.4 数据迁移到新服务器中,并且升级到 12.4.2升级思路:准备一台新服务器168.61.113.220,安装gitlab8.10.4,然后备份生产环境gitlab数据,在新服务器恢复,最后、在进行升级。因为gitlab升级不能跨大版本,所以必须升级到…...

    2024/5/4 0:13:01
  8. 一棵树了解红黑树的性质和定义以及旋转和颜色变换规则

    红黑树的定义和性质 红黑树是一种含有红黑节点并且能自平衡的二叉查找树。满足以下5个性质:每个节点要么是黑色,要么是红色 根节点是黑色 每个子节点(NIL)是黑色 每个红色结点的两个子节点一定是黑色 任意一节点到每个叶子节点的路径都包含数量相同的黑节点性质五可以解释如…...

    2024/5/4 7:55:06
  9. [Swoole] 结合 Laravel 与 Swoole,更简易的开发与布署!

    构想 Swoole 实现了 HTTP Server,也就是说 PHP 的布署不再需要依赖 Nginx 或 Apache 这样的网页伺服器,也不需要靠着 PHP-FPM 或 PHP_mod 才能与 Web Server 沟通。 安装了 Swoole extension 后的 PHP,就像 golang 那般內建高效率的 HTTP Server。 甚至,只要用一个简单的 D…...

    2024/4/12 3:15:52
  10. 用序列图表达超时提醒

    寻找 2018-12-21 11:11如何用序列图表达,在整理资料的过程中,若超时,给出超时提醒UMLChina潘加宇:时间--->检查超时寻找:也就是,需要一个时间--业务实体?UMLChina潘加宇:看第四章寻找:好的UMLChina潘加宇:另外,如果超时提醒的频率和你这个流程频率不一致,应该另外画一…...

    2024/4/27 19:37:19
  11. 学习笔记(26):70讲轻松通关JavaSE-static关键字

    立即学习:https://edu.csdn.net/course/play/30002/432857?utm_source=blogtoedu* static关键字:修饰变量:存储空间只有一份,在对象创建之前就可以通过类名直接访问* 注意:static关键字只能用于修饰成员变量,不能用于修饰局部变量,否则编译会报错* 在方法前加上static关…...

    2024/4/10 11:03:26
  12. 给计算机专业的朋友说几点发自内心的建议

    如今计算机专业是越来越火,无论学校大小,都会有计算机专业。高考结束,很多人都想报考计算机专业,甚至很多很多的人对计算机专业感兴趣。我浅谈几点对于计算机专业的建议,希望对你有所帮助。不对的地方请指正。问题一:计算机专业出来是干嘛的?计算机专业出来说简单一点儿…...

    2024/4/12 6:15:00
  13. 系列文章-随机数在密码学中的作用(四)应用介绍4

    系列文章-随机数在密码学中的作用(四)应用介绍4本期介绍随机数在密码学中生成密钥的用法。在对称加密算法体系中,一次有效密钥有着重要应用,有些应用中也称其动态随机密钥。即通信双方每次交互前先约定一个新的密钥,然后再用该密钥进行信道加密等处理。众所周知,对于对称…...

    2024/4/10 11:03:24
  14. 来千寻三个月感受

    自6月3日入职后,到今天已经快3个月了,此时窗外蒙蒙细雨,感触颇多,写下此篇,为总结,为记录首先先简单介绍下千寻(位于上海),不是千与千寻哦,千寻位置主要是基于北斗卫星提供高精度定位的,平时工作996很少,基本上985,福利待遇可以与其它大厂看齐,工作上与之前公司差别…...

    2024/4/25 0:28:30
  15. mysql学习之旅途(七)(高级部分)

    文章目录mysql逻辑架构解析存储引擎简介性能下降sql慢sql执行顺序索引索引优势和劣势mysql索引分类主键索引单值索引唯一索引复合索引索引结构与检索原理 mysql逻辑架构解析 和其它数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在…...

    2024/5/4 12:30:23
  16. EasyPOI 使用教程

    目录1. 前传1.1 前言1.2 Easypoi介绍1.3 使用1.4 测试项目1.5 可能存在的小坑2. Excel 注解版2.1 Excel导入导出2.2 注解2.3 注解导出,导入2.3.1 对象定义2.3.2 集合定义2.3.3 图片的导出2.3.4 Excel导入介绍2.3.5 Excel导入小功能2.3.6 图片的导入2.3.7 Excel多Sheet导出2.4 注…...

    2024/4/14 2:10:24
  17. 数据结构与算法Python语言实现《Data Structures & Algorithms in Python》手写课后答案--第八章

    第八章 本章重在灵活应用树,对树的重构练习较少slicingtree切片树和博弈树是本章练习稍微难的地方: 切片树:题目中并没有给出图形按什么切割,所以我使用随机切割,见8.67(可食用turtle库完整画出树和图的切割) 博弈树:博弈树的评估算法很简单,比较zz,可以修改评估算法增…...

    2024/5/4 7:24:18
  18. 客户案例 | 静心共享自习室——自习室微信预约解决方案

    开学季,你有没有发现图书馆的座位越来越难抢了?再加上疫情的影响,去图书馆学习的人只能够呈斜对角坐,导致本来就供不应求的座位更加难抢。那么哪里才是学习的好去处呢?付费自习室你值得拥有!用一瓶饮料的钱就可以轻轻松松get舒服且氛围好的学习环境。 静心共享自习室就是…...

    2024/5/2 6:07:01
  19. 8天入门docker系列 —— 第四天 使用aspnetcore小案例熟悉端口映射和挂载目录

    到目前为止大家应该对镜像和容器有了一个大概认知,而且也用了docker进行了一个简单化的部署,但仔细一看问题还有很多,所以这篇我们继续完善。一:如何让外网访问到容器内应用我们知道容器内拥有自己的子网,和你的主机ip不在一个网段内,所以宿主之外的机器是无法访问的,要…...

    2024/4/12 3:15:46
  20. 微服务架构一直火,为什么服务化要搞懂?

    微服务架构,这 5 年左右一直被认可,是软件架构的未来方向。需要大家理解的是,为什么需要服务化。比如微服务架构对企业来说,带来什么价值?有啥弊端?这里浅谈一下微服务架构,主要还是在理解 Why :为什么需要服务化?一、对微服务架构的理解1.1 微服务架构微服务架构,主…...

    2024/4/12 3:15:45

最新文章

  1. STM32(六):定时器PWM呼吸灯 (标准库函数)

    前言 上一篇文章已经介绍了如何用STM32单片机中的TIMER定时器来控制LED灯的交替闪烁&#xff0c;实现了点灯的第五种方式。这篇文章我们来介绍一下如何用STM32单片机中的定时器的PWM波来实现LED的“呼吸”。 一、实验原理 关于定时器这边就不多加赘述&#xff0c;详细请看上…...

    2024/5/8 9:39:08
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/5/7 10:36:02
  3. Python 潮流周刊#44:Mojo 本周开源了;AI 学会生成音乐了

    △△请给“Python猫”加星标 &#xff0c;以免错过文章推送 你好&#xff0c;我是猫哥。这里每周分享优质的 Python、AI 及通用技术内容&#xff0c;大部分为英文。本周刊开源&#xff0c;欢迎投稿[1]。另有电报频道[2]作为副刊&#xff0c;补充发布更加丰富的资讯&#xff0c;…...

    2024/5/7 10:16:32
  4. ntp服务器搭建

    1、手动修改时区 CST可以为如下4个不同的时区的缩写&#xff1a; 美国中部时间&#xff1a;Central Standard Time (USA) UT-6:00 澳大利亚中部时间&#xff1a;Central Standard Time (Australia) UT9:30 中国标准时间&#xff1a;China Standard Time UT8:00 古巴标准时间&a…...

    2024/5/7 8:17:20
  5. 416. 分割等和子集问题(动态规划)

    题目 题解 class Solution:def canPartition(self, nums: List[int]) -> bool:# badcaseif not nums:return True# 不能被2整除if sum(nums) % 2 ! 0:return False# 状态定义&#xff1a;dp[i][j]表示当背包容量为j&#xff0c;用前i个物品是否正好可以将背包填满&#xff…...

    2024/5/7 19:05:20
  6. 【Java】ExcelWriter自适应宽度工具类(支持中文)

    工具类 import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet;/*** Excel工具类** author xiaoming* date 2023/11/17 10:40*/ public class ExcelUti…...

    2024/5/7 22:31:36
  7. Spring cloud负载均衡@LoadBalanced LoadBalancerClient

    LoadBalance vs Ribbon 由于Spring cloud2020之后移除了Ribbon&#xff0c;直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件&#xff0c;我们讨论Spring负载均衡以Spring Cloud2020之后版本为主&#xff0c;学习Spring Cloud LoadBalance&#xff0c;暂不讨论Ribbon…...

    2024/5/8 1:37:40
  8. TSINGSEE青犀AI智能分析+视频监控工业园区周界安全防范方案

    一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;…...

    2024/5/7 14:19:30
  9. VB.net WebBrowser网页元素抓取分析方法

    在用WebBrowser编程实现网页操作自动化时&#xff0c;常要分析网页Html&#xff0c;例如网页在加载数据时&#xff0c;常会显示“系统处理中&#xff0c;请稍候..”&#xff0c;我们需要在数据加载完成后才能继续下一步操作&#xff0c;如何抓取这个信息的网页html元素变化&…...

    2024/5/8 1:37:39
  10. 【Objective-C】Objective-C汇总

    方法定义 参考&#xff1a;https://www.yiibai.com/objective_c/objective_c_functions.html Objective-C编程语言中方法定义的一般形式如下 - (return_type) method_name:( argumentType1 )argumentName1 joiningArgument2:( argumentType2 )argumentName2 ... joiningArgu…...

    2024/5/7 16:57:02
  11. 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】

    &#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格…...

    2024/5/7 14:58:59
  12. 【ES6.0】- 扩展运算符(...)

    【ES6.0】- 扩展运算符... 文章目录 【ES6.0】- 扩展运算符...一、概述二、拷贝数组对象三、合并操作四、参数传递五、数组去重六、字符串转字符数组七、NodeList转数组八、解构变量九、打印日志十、总结 一、概述 **扩展运算符(...)**允许一个表达式在期望多个参数&#xff0…...

    2024/5/7 1:54:46
  13. 摩根看好的前智能硬件头部品牌双11交易数据极度异常!——是模式创新还是饮鸩止渴?

    文 | 螳螂观察 作者 | 李燃 双11狂欢已落下帷幕&#xff0c;各大品牌纷纷晒出优异的成绩单&#xff0c;摩根士丹利投资的智能硬件头部品牌凯迪仕也不例外。然而有爆料称&#xff0c;在自媒体平台发布霸榜各大榜单喜讯的凯迪仕智能锁&#xff0c;多个平台数据都表现出极度异常…...

    2024/5/7 21:15:55
  14. Go语言常用命令详解(二)

    文章目录 前言常用命令go bug示例参数说明 go doc示例参数说明 go env示例 go fix示例 go fmt示例 go generate示例 总结写在最后 前言 接着上一篇继续介绍Go语言的常用命令 常用命令 以下是一些常用的Go命令&#xff0c;这些命令可以帮助您在Go开发中进行编译、测试、运行和…...

    2024/5/8 1:37:35
  15. 用欧拉路径判断图同构推出reverse合法性:1116T4

    http://cplusoj.com/d/senior/p/SS231116D 假设我们要把 a a a 变成 b b b&#xff0c;我们在 a i a_i ai​ 和 a i 1 a_{i1} ai1​ 之间连边&#xff0c; b b b 同理&#xff0c;则 a a a 能变成 b b b 的充要条件是两图 A , B A,B A,B 同构。 必要性显然&#xff0…...

    2024/5/7 16:05:05
  16. 【NGINX--1】基础知识

    1、在 Debian/Ubuntu 上安装 NGINX 在 Debian 或 Ubuntu 机器上安装 NGINX 开源版。 更新已配置源的软件包信息&#xff0c;并安装一些有助于配置官方 NGINX 软件包仓库的软件包&#xff1a; apt-get update apt install -y curl gnupg2 ca-certificates lsb-release debian-…...

    2024/5/7 16:04:58
  17. Hive默认分割符、存储格式与数据压缩

    目录 1、Hive默认分割符2、Hive存储格式3、Hive数据压缩 1、Hive默认分割符 Hive创建表时指定的行受限&#xff08;ROW FORMAT&#xff09;配置标准HQL为&#xff1a; ... ROW FORMAT DELIMITED FIELDS TERMINATED BY \u0001 COLLECTION ITEMS TERMINATED BY , MAP KEYS TERMI…...

    2024/5/8 1:37:32
  18. 【论文阅读】MAG:一种用于航天器遥测数据中有效异常检测的新方法

    文章目录 摘要1 引言2 问题描述3 拟议框架4 所提出方法的细节A.数据预处理B.变量相关分析C.MAG模型D.异常分数 5 实验A.数据集和性能指标B.实验设置与平台C.结果和比较 6 结论 摘要 异常检测是保证航天器稳定性的关键。在航天器运行过程中&#xff0c;传感器和控制器产生大量周…...

    2024/5/7 16:05:05
  19. --max-old-space-size=8192报错

    vue项目运行时&#xff0c;如果经常运行慢&#xff0c;崩溃停止服务&#xff0c;报如下错误 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 因为在 Node 中&#xff0c;通过JavaScript使用内存时只能使用部分内存&#xff08;64位系统&…...

    2024/5/8 1:37:31
  20. 基于深度学习的恶意软件检测

    恶意软件是指恶意软件犯罪者用来感染个人计算机或整个组织的网络的软件。 它利用目标系统漏洞&#xff0c;例如可以被劫持的合法软件&#xff08;例如浏览器或 Web 应用程序插件&#xff09;中的错误。 恶意软件渗透可能会造成灾难性的后果&#xff0c;包括数据被盗、勒索或网…...

    2024/5/8 1:37:31
  21. JS原型对象prototype

    让我简单的为大家介绍一下原型对象prototype吧&#xff01; 使用原型实现方法共享 1.构造函数通过原型分配的函数是所有对象所 共享的。 2.JavaScript 规定&#xff0c;每一个构造函数都有一个 prototype 属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象…...

    2024/5/7 11:08:22
  22. C++中只能有一个实例的单例类

    C中只能有一个实例的单例类 前面讨论的 President 类很不错&#xff0c;但存在一个缺陷&#xff1a;无法禁止通过实例化多个对象来创建多名总统&#xff1a; President One, Two, Three; 由于复制构造函数是私有的&#xff0c;其中每个对象都是不可复制的&#xff0c;但您的目…...

    2024/5/7 7:26:29
  23. python django 小程序图书借阅源码

    开发工具&#xff1a; PyCharm&#xff0c;mysql5.7&#xff0c;微信开发者工具 技术说明&#xff1a; python django html 小程序 功能介绍&#xff1a; 用户端&#xff1a; 登录注册&#xff08;含授权登录&#xff09; 首页显示搜索图书&#xff0c;轮播图&#xff0…...

    2024/5/8 1:37:29
  24. 电子学会C/C++编程等级考试2022年03月(一级)真题解析

    C/C++等级考试(1~8级)全部真题・点这里 第1题:双精度浮点数的输入输出 输入一个双精度浮点数,保留8位小数,输出这个浮点数。 时间限制:1000 内存限制:65536输入 只有一行,一个双精度浮点数。输出 一行,保留8位小数的浮点数。样例输入 3.1415926535798932样例输出 3.1…...

    2024/5/7 17:09:45
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57