系统提供AVFoundation和AVKit两个库,来支持用户实现音视频播放功能。

ios iframe 原生video 视频播放 播放控件加载在中间了 ios播放flv_加载

AVKit是基于AVFoundation进行封装的,提供基本的播放界面,但是AVFoundation可以提供更多高级的功能。,使用AVPlayerController可以很方便的实现一个音视频播放器

func playVideo(_ sender: UIButton) {
        
    guard let url = URL(string: "https://devimages-cdn.apple.com/samplecode/avfoundationMedia/AVFoundationQueuePlayer_HLS2/master.m3u8") else { return };
        
        let player = AVPlayer(url: url);
        
        let controller = AVPlayerViewController();
        controller.player = player;
        
        present(controller, animated: true) {
            player.play();
        }
 }

如果需要更多高级的功能,需要使用AVFoundation类。

下面介绍几个重要的类:

1.Audio Session, 在播放音视频之前,系统会设置一个默认的audio session,它是操作系统和你App进行音频管理的媒介。

默认情况下,它支持音频播放,而不支持录音;当在设置中将响铃设置为静音模式下,音频播放也会静音;设备锁定的时候会静音,当前音频播放的时候其他后台播放的音频也会静音。此时需要设置你需要的Audio Session模式

let audioSession = AVAudioSession.sharedInstance()
    do {
        try audioSession.setCategory(AVAudioSessionCategoryPlayback)
    } catch {
        print("Setting category to AVAudioSessionCategoryPlayback failed.")
    }

设置audioSession的类别为AVAudioSessionCategoryPlayback,代表音频播放是你App的主要功能,它允许在静音模式下播放音频,并且当设置后台运行模式为 Audio, AirPlay, and Picture in Picture时,可以让你的App在后台播放音频。

ios iframe 原生video 视频播放 播放控件加载在中间了 ios播放flv_音视频播放_02

2.AVAssert一个资源类,负责加载音视频资源。多媒体资源的静态模型

ios iframe 原生video 视频播放 播放控件加载在中间了 ios播放flv_AVFoundation_03

AVAssetTrack是一个音视频流的数据模型。

let url: URL = // Local or Remote Asset URL
let asset = AVAsset(url: url)
//AVAsset是一个抽象类,实际使用AVURLAsset来初始化。如果需要更多的控制那么使用AVURLAsset来初始化
let url: URL = // Remote Asset URL
let options = [AVURLAssetAllowsCellularAccessKey: false]//设置移动蜂窝网络下不会读取资源,只有在WiFi网络下才会加载资源
let asset = AVURLAsset(url: url, options: options)

资源的加载是同步的,如果在主线程中加载较大的资源那么可能会导致线程阻塞。可以使用异步加载,来避免这种情况。可以使用

statusOfValueForKey:error:方法来查看加载的进度。由于该方法在background queue里调用,因此,当需要界面的刷新时,需要回到主线程里去执行。

// URL of a bundle asset called 'example.mp4'
let url = Bundle.main.url(forResource: "example", withExtension: "mp4")!
let asset = AVAsset(url: url)
let playableKey = "playable"
 
// Load the "playable" property
asset.loadValuesAsynchronously(forKeys: [playableKey]) {
    var error: NSError? = nil
    let status = asset.statusOfValue(forKey: playableKey, error: &error)
    switch status {
    case .loaded:
        // Sucessfully loaded. Continue processing.
    case .failed:
        // Handle error
    case .cancelled:
        // Terminate processing
    default:
        // Handle all other cases
    }
}

3.AVMetadataItem读取AVAsset的元数据,AVFoundation类将元数据分为两类Fomat-specific keyspaces和 common key space。

一个assert可能包含多种形式的metadata,可以使用metadata属性读取所有形式的元数据。

let url = Bundle.main.url(forResource: "audio", withExtension: "m4a")!
let asset = AVAsset(url: url)
let formatsKey = "availableMetadataFormats"
asset.loadValuesAsynchronously(forKeys: [formatsKey]) {
    var error: NSError? = nil
    let status = asset.statusOfValue(forKey: formatsKey, error: &error)
    if status == .loaded {
        for format in asset.availableMetadataFormats {
            let metadata = asset.metadata(forFormat: format)
            // process format-specific metadata collection
        }
    }
}

获取到metadata以后,需要读取metadata的值:

// Collection of "common" metadata
let metadata = asset.commonMetadata
// Filter metadata to find the asset's artwork
let artworkItems =
    AVMetadataItem.metadataItems(from: metadata,
                                 filteredByIdentifier: AVMetadataCommonIdentifierArtwork)
if let artworkItem = artworkItems.first {
    // Coerce the value to an NSData using its dataValue property
    if let imageData = artworkItem.dataValue {
        let image = UIImage(data: imageData)
        // process image
    } else {
        // No image data found
    }
}

4.AVPlayer,负责播放音视频及时间管理,AVPlayer一次只能播放一个资源,如果需要顺序播放多个资源,可以使用它的子类AVQueuePlayer来管理播放队列。

5.AVPlayerItem,负责和AVPlayer交互,管理媒体资源的动态部分,管理资源的时间和状态。

6.AVKit和AVPlayerLayer负责播放视图界面。

以上类之间的关系可以用一张层次图来表示:

ios iframe 原生video 视频播放 播放控件加载在中间了 ios播放flv_加载_04

创建一个播放器的示例如下:

class PlayerViewController: UIViewController {
 
    @IBOutlet weak var playerViewController: AVPlayerViewController!
 
    var player: AVPlayer!
    var playerItem: AVPlayerItem!
 
    override func viewDidLoad() {
        super.viewDidLoad()
 
        // 1) Define asset URL
        let url: URL = // URL to local or streamed media
 
        // 2) Create asset instance
        let asset = AVAsset(url: url)
 
        // 3) Create player item
        playerItem = AVPlayerItem(asset: asset)
 
        // 4) Create player instance
        player = AVPlayer(playerItem: playerItem)
 
        // 5) Associate player with view controller
        playerViewController.player = player
    }
 
}

可以使用KVO来监控播放的状态:

let url: URL = // Asset URL
 
var asset: AVAsset!
var player: AVPlayer!
var playerItem: AVPlayerItem!
 
// Key-value observing context
private var playerItemContext = 0
 
let requiredAssetKeys = [
    "playable",
    "hasProtectedContent"
]
 
func prepareToPlay() {
    // Create the asset to play
    asset = AVAsset(url: url)
 
    // Create a new AVPlayerItem with the asset and an
    // array of asset keys to be automatically loaded
    playerItem = AVPlayerItem(asset: asset,
                              automaticallyLoadedAssetKeys: requiredAssetKeys)
 
    // Register as an observer of the player item's status property
    playerItem.addObserver(self,
                           forKeyPath: #keyPath(AVPlayerItem.status),
                           options: [.old, .new],
                           context: &playerItemContext)
 
    // Associate the player item with the player
    player = AVPlayer(playerItem: playerItem)
}

override func observeValue(forKeyPath keyPath: String?,
                           of object: Any?,
                           change: [NSKeyValueChangeKey : Any]?,
                           context: UnsafeMutableRawPointer?) {
 
    // Only handle observations for the playerItemContext
    guard context == &playerItemContext else {
        super.observeValue(forKeyPath: keyPath,
                           of: object,
                           change: change,
                           context: context)
        return
    }
 
    if keyPath == #keyPath(AVPlayerItem.status) {
        let status: AVPlayerItemStatus
        if let statusNumber = change?[.newKey] as? NSNumber {
            status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
        } else {
            status = .unknown
        }
        // Switch over status value
        switch status {
        case .readyToPlay:
            // Player item is ready to play.
        case .failed:
            // Player item failed. See error.
        case .unknown:
            // Player item is not yet ready.
        }
    }
}

处理时间相关的操作:监听播放进度,Core Media提供一个低级的API CMTime来表示播放时间,

// 0.25 seconds
let quarterSecond = CMTime(value: 1, timescale: 4)
 
// 10 second mark in a 44.1 kHz audio file
let tenSeconds = CMTime(value: 441000, timescale: 44100)
 
// 3 seconds into a 30fps video
let cursor = CMTime(value: 90, timescale: 30)

player提供两种监听时间的方式:


addPeriodicTimeObserver和addBoundaryTimeObserver来实现每隔一定时间监听,和特定的时间点监听,并且支持按时间查找


var player: AVPlayer!
var playerItem: AVPlayerItem!
var timeObserverToken: Any?
 
func addPeriodicTimeObserver() {
    // Notify every half second
    let timeScale = CMTimeScale(NSEC_PER_SEC)
    let time = CMTime(seconds: 0.5, preferredTimescale: timeScale)
    timeObserverToken = player.addPeriodicTimeObserver(forInterval: time,
                                                       queue: .main) {
        [weak self] time in
        // update player transport UI
    }
}
 
func removePeriodicTimeObserver() {
    if let timeObserverToken = timeObserverToken {
        player.removeTimeObserver(timeObserverToken)
        self.timeObserverToken = nil
    }
}
var asset: AVAsset!
var player: AVPlayer!
var playerItem: AVPlayerItem!
var timeObserverToken: Any?
 
func addBoundaryTimeObserver() {
 
    // Divide the asset's duration into quarters.
    let interval = CMTimeMultiplyByFloat64(asset.duration, 0.25)
    var currentTime = kCMTimeZero
    var times = [NSValue]()
 
    // Calculate boundary times
    while currentTime < asset.duration {
        currentTime = currentTime + interval
        times.append(NSValue(time:currentTime))
    }
 
    timeObserverToken = player.addBoundaryTimeObserver(forTimes: times,
                                                       queue: .main) {
        // Update UI
    }
}
 
func removeBoundaryTimeObserver() {
    if let timeObserverToken = timeObserverToken {
        player.removeTimeObserver(timeObserverToken)
        self.timeObserverToken = nil
    }
}

 

// Seek to the 2 minute mark
let time = CMTime(value: 120, timescale: 1)
player.seek(to: time)
// Seek to the first frame at 3:25 mark
let seekTime = CMTime(seconds: 205, preferredTimescale: Int32(NSEC_PER_SEC))
player.seek(to: seekTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)