AVPlayerの使い方を確認してみる
iOSではどうやって動画はどうやって再生するのか?
弊社のアプリの中で動画を再生するところがあるのですが、動画の再生の実装をしたことなく、 実装方法がわからなかったので、調べてみました。
動画を再生する方法は色々ありますが、今回は、AvPlayer
の基本的な使い方、及びできることをまとめてみました。 今回いろいろ実装したサンプルアプリは、 こちらから確認できます 実装の詳細については、こちらのリポジトリを確認してください。
今回確認したいこと
- HLS(mp4)形式の動画を再生・停止する
- 再生状況を取得する
- 再生速度を変更する
- スライダーで指定の秒数で再生するようにする
- 指定の秒数早送り・巻き戻しする
- 音量を調整する
- iOS14.1
- Xcode12.1
- macOS Catalina v10.15.6
1. HLS(mp4)形式の動画を再生・停止する
そもそもHLSとは、Appleが開発した動画再生の規格です。詳しくは、こちらなどのサイトを確認してみてください。 まずは上記のアプリで実装している箇所を記載します。
private let avPlayer = AVPlayer()
private let avPlayerLayer = AVPlayerLayer()
private var avPlayerItem: AVPlayerItem?
private func setupPlayer() {
// ①AVAssetを作成する
let avAsset = AVAsset(url: URL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8")!)
// ②AvAssetをもとに、AVPlayerItemを作成する
let avPlayerItem = AVPlayerItem(asset: avAsset)
self.avPlayerItem = avPlayerItem
// ③avPlayerItemをavPlayerにセットする
avPlayer.replaceCurrentItem(with: avPlayerItem)
// ④AVPlayerLayerをViewに設定する
playerView.layer.addSublayer(avPlayerLayer)
// ⑤AVPlayerをAVPlayerLayerに設定する
avPlayerLayer.player = avPlayer
avPlayerLayer.frame = playerView.frame
avPlayerLayer.videoGravity = .resizeAspect
avPlayerLayer.needsDisplayOnBoundsChange = true
}
別の設定のコードも含まれてますが、基本的な手順としては、
- AVAssetを作成する
- AvAssetをもとに、AVPlayerItemを作成する
- avPlayerItemをavPlayerにセットする
- AVPlayerLayerをViewに設定する
- AVPlayerをAVPlayerLayerに設定する
をした後に、
avPlayer.play()
を実行すると、設定したサンプル動画が動画が再生されます。 mp4の動画を再生したい場合は、URLをmp4のものに変更すれば、上記と同じ実装で再生することができます。
2. 再生状況を取得する
動画を再生しようとした時に、1.のコードを実行した後、すぐに再生できるわけではありません。 通信環境にもよりますが、最初にある程度ダウンロードした状態にならないと、実際に再生することはできません。
そこで、再生できる状態になっているかどうかのステータスを取得する方法を確認します。 このステータスは、AvPlayerのenumのstatus
プロパティを確認し、これが.readyToPlay
になっていれば、 再生することができます。
なおこのアプリでは検証のため、このプロパティの変化を確認できるように、そのステータスをLabelに出力していますが、 KVO(Key Value Observing)を使って、ステータスの変化を監視しています。
@IBOutlet private weak var statusLabel: UILabel!
private var observers: [NSKeyValueObservation] = []
observers.append(avPlayer.observe(\.status, options: .new, changeHandler: { [unowned self] (avPlayer, value) in
// valueを使うと、nilになる
switch avPlayer.status {
case .readyToPlay: self.statusLabel.text = "Ready to play"
case .failed: self.statusLabel.text = "Failed"
case .unknown: self.statusLabel.text = "unknown"
@unknown default: fatalError()
}
}))
(本当はvalueに変更された後の値が流れてくるはずだが、なぜかnilになるので、avPlayer.status
の値を直接取得している) また再生中/停止中のステータスは、avPlayer.timeControlStatus
で取得できますが、status
と同じようにKVOで変更を監視しています。
@IBOutlet private weak var timeControlStatusLabel: UILabel!
switch avPlayer.timeControlStatus {
case .playing: self.timeControlStatusLabel.text = "Playing"
case .paused: self.timeControlStatusLabel.text = "Paused"
case .waitingToPlayAtSpecifiedRate: self.timeControlStatusLabel.text = "WaitingToPlayAtSpecifiedRate"
@unknown default: fatalError()
}
3. 再生速度を変更する
再生速度は、avPlayer.rate
から確認することができます。 この値を変更することで、再生速度を変更することができます。 1倍速の時は、1に設定し、2倍速にしたい時は、2に設定します。 このアプリでは、segmentの切り替えをトリガーに、0.5/1/2倍速を切り替えられるようにしています。
@IBAction private func onPlaySpeedValueChanged(_ sender: UISegmentedControl) {
if sender.selectedSegmentIndex == 0 {
avPlayer.rate = 0.5
} else if sender.selectedSegmentIndex == 1 {
avPlayer.rate = 1
} else if sender.selectedSegmentIndex == 2 {
avPlayer.rate = 2
}
}
4. スライダーで指定の秒数で再生するようにする
これはアプリスクショのControlセクションにあるSliderを動かすと、その進捗具合に合わせて、動画の再生時間を変更させる機能になります。 AVPlayerではこのあたりの機能を使う時は、CMTime
などのstructを使います。(参考)
@IBAction func onDurationSliderValueChanged(_ sender: UISlider, forEvent event: UIEvent) {
guard let touchEvent = event.allTouches?.first else { return }
switch touchEvent.phase {
case .began:
isSliderDragging = true
case .ended:
// ①スライダーの値から、目標の再生時間を求める
let seconds = Double(sender.value) * itemDuration
let timeScale = CMTimeScale(NSEC_PER_SEC)
// 目標の再生時間から、実際に再生時間を変更するための、`CMTime`を生成する。
let time = CMTime(seconds: seconds, preferredTimescale: timeScale)
changePosition(time: time)
isSliderDragging = false
case .cancelled:
isSliderDragging = false
default: break
}
}
private func changePosition(time: CMTime) {
// timeの値をもとに、SliderのUIを更新する
updateDurationSlider(time: time)
let rate = avPlayer.rate
// 時間変更中は、一時的に停止する
avPlayer.rate = 0
avPlayer.seek(to: time) { [weak self] completed in
if !completed { return }
// 変更が完了したら、再び再生する
self?.avPlayer.rate = rate
}
}
5. 指定の秒数早送り・巻き戻しする
この機能は、4.の機能と同じように、現在の再生時間から指定時間早送り、巻き戻しするものです。 4.の実装と共通している箇所もあります。 このアプリでは検証のため、10/15/30秒を指定できるようにしています。
private var offsetDefaultTime: Double = 15
@IBAction func back(_ sender: Any) {
offsetTime(offset: -offsetDefaultTime)
}
@IBAction func forward(_ sender: Any) {
offsetTime(offset: offsetDefaultTime)
}
private func offsetTime(offset: Double) {
let timeScale = CMTimeScale(NSEC_PER_SEC)
// 加減したい時間(秒数)をもとに、`CMTime`のstructを作成する
let rhs = CMTime(seconds: offset, preferredTimescale: timeScale)
// 現在のtime(avPlayer.currentTime())に、offset分の秒数を加減する。
let time = CMTimeAdd(avPlayer.currentTime(), rhs)
changePosition(time: time)
}
6. 音量を調整する
ここではスライダーの値に合わせて、動画の音量を調整する実装を行います。 avPlayer.volume
(最小値が0、最大値が1)の値を変更することで、動画の音量を変更することができます。
@IBAction func onVolumeSliderValueChanged(_ sender: UISlider) {
setVolume(volume: sender.value)
}
private func setVolume(volume: Float) {
avPlayer.volume = volume
volumeSlider.value = volume
}
まとめ
調査と実装をする中で、基本的な使い方は大まかに把握できました。 ただもっと細かい設定など、色々できるので、そこは今後開発などをしながら、少しずつ掴んでいければと思います。