iOS开发基础63-远程音频与视频播放:AVPlayer、AVPlayerLayer、AVPlayerViewController 与 MediaPlayer

iOS 远程音频与视频播放深度解析:AVPlayer、AVPlayerLayer、AVPlayerViewController 与 MediaPlayer

本文系统梳理 iOS 远程音频与视频播放:AVPlayer 播放远程流媒体(音频/视频)的完整配置(KVO 状态监听、进度监听、缓冲进度、播放完成、后台播放、ATS)、AVPlayerLayer 视频渲染与填充模式、AVPlayerViewController(现代推荐,自带播放控制 UI)、以及 MediaPlayer 框架(MPMoviePlayerController/MPMoviePlayerViewController,已废弃)的历史用法与迁移。


一、远程音频播放(AVPlayer)

1. AVPlayer 简介

AVAudioPlayer 只支持本地文件,不支持远程流媒体。播放远程音频(网络 URL)需要使用 AVPlayer(AVFoundation 框架)。

AVPlayer 既可以播放音频,也可以播放视频,支持本地和远程 URL,是 iOS 流媒体播放的核心类。

2. 基本使用

#import <AVFoundation/AVFoundation.h>

@interface ViewController ()
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerItem *currentItem;
@end

@implementation ViewController

- (AVPlayer *)player {
    if (!_player) {
        // 远程音频 URL(建议 HTTPS,HTTP 需配置 ATS 例外)
        NSURL *url = [NSURL URLWithString:@"https://example.com/audio.m4a"];
        
        // 方式一:直接用 URL 创建
        // _player = [[AVPlayer alloc] initWithURL:url];
        
        // 方式二:用 AVPlayerItem 包装(推荐,便于监听状态和切换曲目)
        AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:url];
        _player = [[AVPlayer alloc] initWithPlayerItem:item];
        self.currentItem = item;
    }
    return _player;
}

// 播放
- (IBAction)play {
    [self.player play];
}

// 暂停
- (IBAction)pause {
    [self.player pause];
}

// 更换音频
- (void)replaceAudioWithURL:(NSURL *)url {
    AVPlayerItem *newItem = [[AVPlayerItem alloc] initWithURL:url];
    [self.player replaceCurrentItemWithPlayerItem:newItem];
    self.currentItem = newItem;
}

@end

ATS(App Transport Security):iOS 9+ 默认禁止 HTTP 明文请求,远程 URL 应使用 HTTPS。如果必须用 HTTP,需在 Info.plist 中配置 NSAppTransportSecurity → NSAllowsArbitraryLoads = YES(或针对特定域名配置例外)。

3. 配置后台播放

远程音频通常需要支持后台播放,配置方式与本地音乐相同:

  1. Info.plist 添加 UIBackgroundModes = audio。
  2. 设置 AVAudioSession 类别为 Playback:
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
[session setActive:YES error:nil];

4. 播放状态监听(KVO)

AVPlayer 的状态通过 KVO 监听,不能直接读取(初始化时 status 为 AVPlayerStatusUnknown):

// 监听 AVPlayer 的 status
[self.player addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
// 监听当前播放项的 status
[self.currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
// 监听缓冲进度(loadedTimeRanges)
[self.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
// 监听播放速率变化(timeControlStatus,iOS 10+)
[self.player addObserver:self forKeyPath:@"timeControlStatus" options:NSKeyValueObservingOptionNew context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"status"]) {
        if (object == self.player) {
            // AVPlayer status
            if (self.player.status == AVPlayerStatusReadyToPlay) {
                NSLog(@"准备播放,可以调用 play");
            } else if (self.player.status == AVPlayerStatusFailed) {
                NSLog(@"播放失败: %@", self.player.error);
            }
        } else if (object == self.currentItem) {
            // AVPlayerItem status
            if (self.currentItem.status == AVPlayerItemStatusReadyToPlay) {
                NSLog(@"当前曲目准备就绪");
            }
        }
    } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
        // 缓冲进度
        NSArray *ranges = self.currentItem.loadedTimeRanges;
        if (ranges.count > 0) {
            CMTimeRange timeRange = [ranges.firstObject CMTimeRangeValue];
            NSTimeInterval bufferTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration);
            NSTimeInterval duration = CMTimeGetSeconds(self.currentItem.duration);
            NSLog(@"缓冲进度: %.0f%%", bufferTime / duration * 100);
        }
    } else if ([keyPath isEqualToString:@"timeControlStatus"]) {
        // iOS 10+:播放/暂停/等待状态
        switch (self.player.timeControlStatus) {
            case AVPlayerTimeControlStatusPlaying:
                NSLog(@"正在播放");
                break;
            case AVPlayerTimeControlStatusPaused:
                NSLog(@"已暂停");
                break;
            case AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate:
                NSLog(@"等待缓冲中...");
                break;
        }
    }
}

- (void)dealloc {
    [self.player removeObserver:self forKeyPath:@"status"];
    [self.currentItem removeObserver:self forKeyPath:@"status"];
    [self.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
    [self.player removeObserver:self forKeyPath:@"timeControlStatus"];
}

5. 播放进度监听

// 定期监听播放进度(每秒回调一次)
__weak typeof(self) weakSelf = self;
self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1)
                                                                 queue:dispatch_get_main_queue()
                                                            usingBlock:^(CMTime time) {
    NSTimeInterval current = CMTimeGetSeconds(time);
    NSTimeInterval duration = CMTimeGetSeconds(weakSelf.currentItem.duration);
    NSLog(@"进度: %.0f / %.0f 秒", current, duration);
    // 更新进度条
    // weakSelf.progressView.progress = current / duration;
}];

// 移除进度监听
- (void)dealloc {
    if (self.timeObserver) {
        [self.player removeTimeObserver:self.timeObserver];
    }
}

6. 播放完成通知

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(playerItemDidPlayToEnd:)
                                             name:AVPlayerItemDidPlayToEndTimeNotification
                                           object:self.currentItem];

- (void)playerItemDidPlayToEnd:(NSNotification *)note {
    NSLog(@"播放完成,可以播放下一首或循环");
    // 循环播放:
    // [self.player seekToTime:kCMTimeZero completionHandler:^(BOOL finished) {
    //     [self.player play];
    // }];
}

7. 跳转播放位置

// 跳转到指定时间(秒)
- (void)seekToTime:(NSTimeInterval)seconds {
    CMTime time = CMTimeMakeWithSeconds(seconds, NSEC_PER_SEC);
    [self.player seekToTime:time completionHandler:^(BOOL finished) {
        NSLog(@"跳转完成");
    }];
}

二、视频播放(AVPlayer + AVPlayerLayer)

1. 基本使用

AVPlayer 播放视频时,需要用 AVPlayerLayer 将视频画面渲染到屏幕上:

#import <AVFoundation/AVFoundation.h>

@interface ViewController ()
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 创建 URL(远程或本地)
    NSURL *url = [NSURL URLWithString:@"https://example.com/video.mp4"];
    // 本地视频:
    // NSURL *url = [[NSBundle mainBundle] URLForResource:@"video" withExtension:@"mp4"];
    
    // 2. 创建 AVPlayer
    AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:url];
    self.player = [[AVPlayer alloc] initWithPlayerItem:item];
    
    // 3. 创建 AVPlayerLayer(渲染视频画面)
    self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    [self.view.layer addSublayer:self.playerLayer];
}

// frame 应在 viewDidLayoutSubviews 中设置(约束布局完成后)
- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    // 16:9 视频比例
    CGFloat width = self.view.bounds.size.width;
    self.playerLayer.frame = CGRectMake(0, 100, width, width * 9 / 16);
}

- (IBAction)play {
    [self.player play];
}

@end

布局时机:不要在 viewDidLoad 中用 self.view.bounds 设置 AVPlayerLayer 的 frame,此时约束还未完成布局,尺寸可能不准确。应在 viewDidLayoutSubviews 中设置。

2. 视频填充模式(videoGravity)

AVPlayerLayer 有三种视频填充模式:

videoGravity 效果 说明
AVLayerVideoGravityResizeAspect 保持比例,完整显示,留黑边 默认值
AVLayerVideoGravityResizeAspectFill 保持比例,填满区域,裁剪超出部分 类似 UIImageView 的 AspectFill
AVLayerVideoGravityResize 拉伸填满,不保持比例,可能变形 类似 UIImageView 的 ScaleToFill
self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;

三、AVPlayerViewController(现代推荐)

1. 简介

手动用 AVPlayer + AVPlayerLayer 实现视频播放需要自己写播放控制 UI(进度条、播放/暂停、全屏、AirPlay 等),工作量大。

iOS 8+ 苹果提供了 AVPlayerViewController(AVKit 框架),它自带完整的播放控制 UI,是现代视频播放的推荐方案,替代已废弃的 MPMoviePlayerViewController。

2. 基本使用

#import <AVKit/AVKit.h>
#import <AVFoundation/AVFoundation.h>

@interface ViewController ()
@property (nonatomic, strong) AVPlayerViewController *playerVC;
@end

@implementation ViewController

- (void)playVideo {
    NSURL *url = [NSURL URLWithString:@"https://example.com/video.mp4"];
    AVPlayer *player = [[AVPlayer alloc] initWithURL:url];
    
    // 创建 AVPlayerViewController
    self.playerVC = [[AVPlayerViewController alloc] init];
    self.playerVC.player = player;
    
    // 设置是否显示播放控制(默认 YES)
    self.playerVC.showsPlaybackControls = YES;
    
    // 是否允许画中画(iPad,iOS 9+)
    self.playerVC.allowsPictureInPicturePlayback = YES;
    
    // 进入全屏时是否隐藏状态栏
    self.playerVC.updatesNowPlayingInfoCenter = YES;
    
    // modal 弹出(全屏播放)
    [self presentViewController:self.playerVC animated:YES completion:^{
        [player play];
    }];
    
    // 或 push 到导航控制器(非全屏)
    // [self.navigationController pushViewController:self.playerVC animated:YES];
}

@end

3. 嵌入子视图(非全屏)

AVPlayerViewController 也可以作为子控制器嵌入,实现小窗口播放:

// 添加为子控制器
[self addChildViewController:self.playerVC];
self.playerVC.view.frame = CGRectMake(0, 100, self.view.bounds.size.width, 200);
[self.view addSubview:self.playerVC.view];
[self.playerVC didMoveToParentViewController:self];

4. AVPlayerViewController 优势

特性 说明
自带播放控制 UI 进度条、播放/暂停、快进快退、全屏、AirPlay、画中画
支持 modal 和 push 可全屏也可嵌入导航栈
支持画中画(PiP) iPad 上可悬浮小窗口播放
系统级优化 硬件解码、后台播放、锁屏控制
维护成本低 不需要自己写控制 UI

四、MediaPlayer 框架(已废弃)

1. 简介

MediaPlayer 框架提供了 MPMoviePlayerController 和 MPMoviePlayerViewController 两个类,用于早期 iOS 的视频播放。

重要:这两个类在 iOS 9 被苹果废弃(deprecated),现代开发应使用 AVPlayerViewController(AVKit 框架)替代。以下内容仅作为历史知识和老项目维护参考。

2. MPMoviePlayerController

MPMoviePlayerController 继承自 NSObject,内部有一个 view 用于展示视频,可以添加到任意控制器的 view 上,支持非全屏播放:

#import <MediaPlayer/MediaPlayer.h>

@interface ViewController ()
@property (nonatomic, strong) MPMoviePlayerController *moviePlayer;
@end

@implementation ViewController

- (MPMoviePlayerController *)moviePlayer {
    if (!_moviePlayer) {
        NSURL *url = [NSURL URLWithString:@"https://example.com/video.mp4"];
        _moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:url];
        _moviePlayer.view.frame = CGRectMake(0, 100, self.view.bounds.size.width, 200);
        // 横竖屏自动调整
        _moviePlayer.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        [self.view addSubview:_moviePlayer.view];
    }
    return _moviePlayer;
}

- (void)play {
    [self.moviePlayer play];
}

// 进入全屏
- (void)fullscreen {
    [self.moviePlayer setFullscreen:YES animated:YES];
}

@end

3. MPMoviePlayerViewController

MPMoviePlayerViewController 是一个视图控制器,只能全屏播放,通过 modal 方式弹出:

#import <MediaPlayer/MediaPlayer.h>

@interface ViewController ()
@property (nonatomic, strong) MPMoviePlayerViewController *playerVC;
@end

@implementation ViewController

- (MPMoviePlayerViewController *)playerVC {
    if (!_playerVC) {
        NSURL *url = [NSURL URLWithString:@"https://example.com/video.mp4"];
        _playerVC = [[MPMoviePlayerViewController alloc] initWithContentURL:url];
    }
    return _playerVC;
}

// 只能 modal 全屏播放
- (void)play {
    [self presentMoviePlayerViewControllerAnimated:self.playerVC];
}

@end

presentMoviePlayerViewControllerAnimated: 是 UIViewController 的 MediaPlayer 分类方法,用于弹出 MPMoviePlayerViewController。

4. 常用监听通知

MPMoviePlayerController 的播放状态通过通知中心监听:

通知名 触发时机
MPMoviePlayerPlaybackStateDidChangeNotification 播放状态变化(播放/暂停/中断)
MPMoviePlayerPlaybackDidFinishNotification 播放完成
MPMoviePlayerDidExitFullscreenNotification 退出全屏
MPMoviePlayerThumbnailImageRequestDidFinishNotification 截屏完成
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(playbackStateChanged:)
                                             name:MPMoviePlayerPlaybackStateDidChangeNotification
                                           object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(playbackFinished:)
                                             name:MPMoviePlayerPlaybackDidFinishNotification
                                           object:nil];

// 截屏
[self.moviePlayer requestThumbnailImagesAtTimes:@[@(1.0), @(5.0)] timeOption:MPMovieTimeOptionExact];

5. 从 MediaPlayer 迁移到 AVKit

MediaPlayer(已废弃) AVKit(推荐)
MPMoviePlayerController AVPlayer + AVPlayerLayer(自定义 UI)或 AVPlayerViewController
MPMoviePlayerViewController AVPlayerViewController
play / pause / stop [player play] / [player pause](无 stop,用 pause + seekToTime:kCMTimeZero)
fullscreen 属性 AVPlayerViewController 自动支持全屏按钮
通知监听状态 KVO 监听 status/timeControlStatus + AVPlayerItemDidPlayToEndTimeNotification

五、视频格式与第三方解码

1. iOS 原生支持的格式

类别 支持的格式
视频编码 H.264(AVC)、HEVC(H.265,iOS 11+)
音频编码 AAC、MP3、Apple Lossless、Linear PCM
容器格式 MP4、M4V、MOV(QuickTime)

关于 AVI 和 MKV:iOS 原生不支持 AVI 和 MKV 容器格式(虽然 MPMoviePlayerController 可能部分兼容,但不可靠)。播放这些格式需要第三方解码库。

2. 第三方解码框架

框架 说明
VLCKit VLC 官方 iOS 库,基于 FFmpeg,支持几乎所有视频格式
FFmpeg 最流行的开源音视频解码库,功能强大但集成复杂
IJKPlayer 哔哩哔哩开源的基于 FFmpeg 的播放器,国内广泛使用
KSYMediaPlayer 金山云开源播放器,基于 IJKPlayer 优化

VLC for iOS 地址:https://github.com/videolan/vlc


六、Swift 版本对照

远程音频播放(Swift)

import AVFoundation

let url = URL(string: "https://example.com/audio.m4a")!
let item = AVPlayerItem(url: url)
let player = AVPlayer(playerItem: item)
player.play()

// 进度监听
player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1),
                                queue: .main) { time in
    let current = CMTimeGetSeconds(time)
    print("进度: \(current)")
}

// 播放完成
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
                                       object: item, queue: .main) { _ in
    print("播放完成")
}

视频播放 AVPlayerLayer(Swift)

import AVFoundation

let url = URL(string: "https://example.com/video.mp4")!
let player = AVPlayer(url: url)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(playerLayer)

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    playerLayer.frame = CGRect(x: 0, y: 100, width: view.bounds.width, height: view.bounds.width * 9 / 16)
}

player.play()

AVPlayerViewController(Swift)

import AVKit
import AVFoundation

let url = URL(string: "https://example.com/video.mp4")!
let player = AVPlayer(url: url)
let playerVC = AVPlayerViewController()
playerVC.player = player
playerVC.showsPlaybackControls = true
present(playerVC, animated: true) {
    player.play()
}

七、总结

  • 远程音频播放:AVAudioPlayer 只支持本地文件,远程流媒体用 AVPlayer;AVPlayer 支持本地和远程 URL,既播音频也播视频;用 AVPlayerItem 包装 URL 便于状态监听和切换曲目;replaceCurrentItemWithPlayerItem 更换曲目。
  • AVPlayer 完整配置:① KVO 监听 status(AVPlayerStatusReadyToPlay/Failed)、timeControlStatus(Playing/Paused/Waiting,iOS 10+)、loadedTimeRanges(缓冲进度);② addPeriodicTimeObserver 监听播放进度;③ AVPlayerItemDidPlayToEndTimeNotification 播放完成;④ seekToTime 跳转;⑤ AVAudioSession Playback + Info.plist UIBackgroundModes=audio 支持后台播放;⑥ ATS 限制(HTTP 需例外,推荐 HTTPS)。
  • 视频播放 AVPlayerLayer:AVPlayer + AVPlayerLayer 渲染视频画面;frame 应在 viewDidLayoutSubviews 设置(非 viewDidLoad);videoGravity 三种模式:ResizeAspect(默认,留黑边)、ResizeAspectFill(填充裁剪)、Resize(拉伸变形)。
  • AVPlayerViewController(现代推荐):iOS 8+,AVKit 框架,自带完整播放控制 UI(进度条、播放/暂停、全屏、AirPlay、画中画),支持 modal 和 push,支持画中画(PiP),替代已废弃的 MPMoviePlayerViewController。
  • MediaPlayer 框架(已废弃):MPMoviePlayerController(非全屏,view 添加到父视图)和 MPMoviePlayerViewController(只能全屏 modal),iOS 9 废弃,推荐迁移到 AVKit;播放状态通过通知中心监听(MPMoviePlayerPlaybackStateDidChangeNotification 等);requestThumbnailImagesAtTimes 截屏。
  • 视频格式:iOS 原生支持 H.264/HEVC(H.265) 视频编码、AAC/MP3 音频编码、MP4/M4V/MOV 容器;不原生支持 AVI/MKV,需第三方解码(VLCKit/FFmpeg/IJKPlayer)。
  • 迁移指南:MPMoviePlayerController → AVPlayer+AVPlayerLayer 或 AVPlayerViewController;MPMoviePlayerViewController → AVPlayerViewController;通知监听 → KVO + AVPlayerItemDidPlayToEndTimeNotification。

posted @ 2015-10-12 00:06  Mr.陳  阅读(1685)  评论(0)    收藏  举报