AVPlayer 制作一个简单的在线音乐播放器

0AC7D6DB-AC44-45E6-A457-2D1517F1DF3F.png

歌手图片旋转:
先设置一个全局变量动画:

{
    CABasicAnimation* _rotationAnimation; /**< 歌手图片旋转动画 */
}

初始化动画操作,歌手图片旋转动画

   // 歌手图片动画效果
    _rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    _rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2];
    _rotationAnimation.duration = kTimerInterval;
    _rotationAnimation.cumulative = YES;
    _rotationAnimation.repeatCount = CGFLOAT_MAX; // 设置旋转次数

当歌曲开始播放的时候,将动画操作添加到歌手图片ImageView

[self.playerImageView.layer addAnimation:_rotationAnimation forKey:@"rotationAnimation"];

这样,类似网易云音乐歌手图片旋转的效果出来了

接下来就是播放器的实现了,在这里使用的是AVPlayer来实现网络音乐播放
AVPlayer中含有一个属性:AVPlayerItem,这个是媒体资源,包含媒体的总时间、缓冲情况等

1.初始化一个AVPlayer

   NSURL *url = [NSURLURLWithString:@"http://mr7.doubanio.com/3fd082ae3370d22e48e300ab1d5d6590/1/fm/song/p190415_128k.mp4"];
   self.songItem = [AVPlayerItemplayerItemWithURL:url];
   self.player = [AVPlayerplayerWithPlayerItem:self.songItem];

2.播放音乐

[self.playerplay];

3.暂停音乐

[self.playerpause];

4.一个简单的在线播放网络音乐就完成了,接下来需要显示歌曲的时间,首先添加一个监听媒体资源的加载情况

// 监听媒体资源缓冲情况
[self.songItem addObserver:selfforKeyPath:@"loadedTimeRanges"options:NSKeyValueObservingOptionNewcontext:nil];

5.在媒体加载时响应监听方法时获取到歌曲的总时间,在这里,只需要获取一次,由于只有一首歌,所以使用GCD的方式去获取歌曲总时间

/**
 *  KVO监听方法
 *
 *  @param keyPath
 *  @param object
 *  @param change
 *  @param context
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    AVPlayerItem *songItem = object;
    if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
        // 媒体总时长
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            self.songTime = CMTimeGetSeconds(songItem.duration); // 获取到媒体的总时长
        });
        // 媒体缓冲
        NSArray *array=songItem.loadedTimeRanges;
        CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次缓冲时间的范围
        float startSeconds = CMTimeGetSeconds(timeRange.start);
        float durationSeconds = CMTimeGetSeconds(timeRange.duration);
        NSTimeInterval totalBuffer = startSeconds + durationSeconds;//缓冲的总长度
        NSLog(@"共缓冲:%.3f, %lf",totalBuffer, CMTimeGetSeconds(songItem.duration));
    }
}

获取到了歌曲的时间,我们还需要显示当前歌曲播放的时间,根据获取到的时间去实现进度条的改变

__weak typeof(self) weakSelf = self;
// 监听播放进度
self.timeObserve = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        CGFloat current = CMTimeGetSeconds(time);
        weakSelf.currentTime = current;
 }];

到了这里,可以根据获得到的时间来计算进度条的value,接下来是根据通过改变进度条的value来实现播放器播放进度,给进度条添加一个target事件

// 监听进度条的手动改变
[self.progress addTarget:self action:@selector(progressValueChage) forControlEvents:UIControlEventValueChanged];

当拖动进度条的监听方法去实现控制播放器的播放进度

/**
 *  进度条改变操作
 */
- (void)progressValueChage {
    CGFloat currentTime = self.songTimes * self.progress.value;
    [self.player changeProgress:currentTime];
}
/**
 *  手动改变进度
 *
 *  @param second 当前播放时间
 */
- (void)changeProgress:(CGFloat)second {
    [self.player pause];
    [self.player seekToTime:CMTimeMakeWithSeconds(second, NSEC_PER_SEC) completionHandler:^(BOOL finished) {
        [self.player play];
    }];
}

这样一个简单的播放器网络音乐就完成了
最后附上完整的代码
ViewController.m文件

#import "ViewController.h"
#import "GPPlayer.h"
#import "UIView+Category.h"

#define kImageViewWidth 230
#define kTimerInterval 20

@interface ViewController ()

@property (nonatomic, strong) GPPlayer *player; /**< 播放器 */
@property (weak, nonatomic) IBOutlet UILabel *songTimeLabel;
@property (weak, nonatomic) IBOutlet UILabel *currentTime;
@property (nonatomic, weak) UIImageView *playerImageView; /**< 歌手图片 */
@property (weak, nonatomic) IBOutlet UISlider *progress; /**< 进度条 */
@property (nonatomic, assign) CGFloat songTimes; /**< 歌曲总时间 */

@end

@implementation ViewController

{
    CABasicAnimation* _rotationAnimation; /**< 歌手图片旋转动画 */
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // 歌手图片
    UIImageView *playerImageView = [[UIImageView alloc] init];
    playerImageView.y = 50;
    playerImageView.size = CGSizeMake(kImageViewWidth, kImageViewWidth);
    playerImageView.centerX = self.view.centerX;
    playerImageView.image = [UIImage imageNamed:@"xufei.jpg"];
    playerImageView.layer.cornerRadius = kImageViewWidth * 0.5;
    playerImageView.layer.masksToBounds = YES;
    [self.view addSubview:playerImageView];
    self.playerImageView = playerImageView;
    
    // 歌手图片动画效果
    _rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    _rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2];
    _rotationAnimation.duration = kTimerInterval;
    _rotationAnimation.cumulative = YES;
    _rotationAnimation.repeatCount = CGFLOAT_MAX; // 设置旋转次数
    
    // 监听进度条的手动改变
    [self.progress addTarget:self action:@selector(progressValueChage) forControlEvents:UIControlEventValueChanged];
}

/**
 *  播放
 *
 *  @param sender
 */
- (IBAction)play:(UIButton *)sender {
    sender.showsTouchWhenHighlighted = YES;
    [self.player play];
    // 旋转歌手图片
    [self imageViewRotate];
}

/**
 *  播放
 *
 *  @param sender
 */
- (IBAction)pause:(UIButton *)sender {
    [self.player pause];
    [self.playerImageView.layer removeAnimationForKey:@"rotationAnimation"];
}

#pragma mark - 自定义方法

/**
 *  歌手图片旋转动画
 */
- (void)imageViewRotate {
    [self.playerImageView.layer addAnimation:_rotationAnimation forKey:@"rotationAnimation"];
}

/**
 *  时间转换
 *
 *  @param time 秒数
 *
 *  @return
 */
- (NSString *)convertStringWithTime:(float)time {
    if (isnan(time)) time = 0.f;
    int min = time / 60.0;
    int sec = time - min * 60;
    NSString * minStr = min > 9 ? [NSString stringWithFormat:@"%d",min] : [NSString stringWithFormat:@"0%d",min];
    NSString * secStr = sec > 9 ? [NSString stringWithFormat:@"%d",sec] : [NSString stringWithFormat:@"0%d",sec];
    NSString * timeStr = [NSString stringWithFormat:@"%@:%@",minStr, secStr];
    return timeStr;
}

#pragma mark - KVO
/**
 *  观察者监听方法
 *
 *  @param keyPath
 *  @param object
 *  @param change
 *  @param context  
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"songTime"]) {
        self.songTimes = self.player.songTime;
        self.songTimeLabel.text = [self convertStringWithTime:self.player.songTime];
    } else if ([keyPath isEqualToString:@"currentTime"]) {
        self.currentTime.text = [self convertStringWithTime:self.player.currentTime];
        CGFloat pro = self.player.currentTime / self.songTimes; /**< 播放进度 */
        self.progress.value = pro;
    }
}

/**
 *  进度条改变操作
 */
- (void)progressValueChage {
    CGFloat currentTime = self.songTimes * self.progress.value;
    [self.player changeProgress:currentTime];
}

#pragma mark - 懒加载

/**
 *  播放器
 *
 *  @return
 */
- (GPPlayer *)player {
    if (_player == nil) {
        _player = [[GPPlayer alloc] init];
        [_player addObserver:self forKeyPath:@"songTime" options:NSKeyValueObservingOptionNew context:nil];
        [_player addObserver:self forKeyPath:@"currentTime" options:NSKeyValueObservingOptionNew context:nil];
    }
    
    return _player;
}

@end

GPPlayer.h

@interface GPPlayer : NSObject

@property (nonatomic, assign) CGFloat songTime; /**< 歌曲总时间 */
@property (nonatomic, assign) CGFloat currentTime; /**< 当前播放时间 */

/**
 *  播放音乐
 */
- (void)play;

/**
 *  暂停播放
 */
- (void)pause;

/**
 *  手动改变播放进度
 */
- (void)changeProgress:(CGFloat)second;

@end

GPPlayer.m

#import "GPPlayer.h"
#import <AVFoundation/AVFoundation.h>

@interface GPPlayer ()

@property (nonatomic, strong) AVPlayer *player; /**< 播放器 */
@property (nonatomic, strong) AVPlayerItem *songItem;
@property (nonatomic, strong) id timeObserve;

@end

@implementation GPPlayer

- (instancetype)init {
    if (self = [super init]) {
        [self setup];
    }
    
    return self;
}

/**
 *  初始化播放器
 */
- (void)setup {
    NSURL *url = [NSURL URLWithString:@"http://mr7.doubanio.com/3fd082ae3370d22e48e300ab1d5d6590/1/fm/song/p190415_128k.mp4"];
    self.songItem = [AVPlayerItem playerItemWithURL:url];
    self.player = [AVPlayer playerWithPlayerItem:self.songItem];
    [self addObserver];
}

/**
 *  添加观察者
 */
- (void)addObserver {
    // 监听媒体资源的状态
    [self.songItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    // 监听媒体资源缓冲情况
    [self.songItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    __weak typeof(self) weakSelf = self;
    // 监听播放进度
    self.timeObserve = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        CGFloat current = CMTimeGetSeconds(time);
        weakSelf.currentTime = current;
    }];
    // 监听播放是否完成
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerFinish:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
}

/**
 *  KVO监听方法
 *
 *  @param keyPath
 *  @param object
 *  @param change
 *  @param context
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    AVPlayerItem *songItem = object;
    if ([keyPath isEqualToString:@"status"]) {
        // 媒体加载
        switch (self.player.status) {
            case AVPlayerStatusFailed:
                NSLog(@"KVO:加载失败,网络或者服务器出现问题");
                break;
                case AVPlayerStatusReadyToPlay:
                NSLog(@"KVO:准备完毕,可以播放");
                break;
                case AVPlayerStatusUnknown:
                NSLog(@"KVO:未知状态");
                break;
        }
    } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
        // 媒体总时长
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            self.songTime = CMTimeGetSeconds(songItem.duration); // 获取到媒体的总时长
        });
        // 媒体缓冲
        NSArray *array=songItem.loadedTimeRanges;
        CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次缓冲时间范围
        float startSeconds = CMTimeGetSeconds(timeRange.start);
        float durationSeconds = CMTimeGetSeconds(timeRange.duration);
        NSTimeInterval totalBuffer = startSeconds + durationSeconds;//缓冲总长度
        NSLog(@"共缓冲:%.2f, %lf",totalBuffer, CMTimeGetSeconds(songItem.duration));
    }
}

/**
 *  监听播放器播放完成
 *
 *  @param sender 
 */
- (void)playerFinish:(NSNotification *)sender {
    NSLog(@"播放完成");
}

/**
 *  手动改变进度
 *
 *  @param second 当前播放时间
 */
- (void)changeProgress:(CGFloat)second {
    [self.player pause];
    [self.player seekToTime:CMTimeMakeWithSeconds(second, NSEC_PER_SEC) completionHandler:^(BOOL finished) {
        [self.player play];
    }];
}

#pragma mark - 播放器操作

/**
 *  播放音乐
 */
- (void)play {
    [self.player play];
}

/**
 *  暂停播放
 */
- (void)pause {
    [self.player pause];
}

@end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容