この記事で学ぶこと:
AVPlayerによる動画再生
①を読んでなくても問題ないですが、概念が曖昧というか方は①から読まれるのをおすすめします。👇

Contents 非表示
AVPlayerによる動画再生
1.1 HLS (m3u8) 再生の仕組みをストーリー式で理解してみる (RPG)
(GPTに考えてもらいました)
ある日、AVPlayer王国に暮らす3人のキャラたちが集まった..
- 👑 AVPlayer(王様)
- 🧙♂️ AVPlayerItem(参謀)
- 🧳 AVAsset(旅の商人)

再生の命令を出すのがワシの役目じゃ

動画の内容・進捗・バッファの状態…すべての情報は私が管理いたします

動画ファイルの在処、私がご案内しましょう…m3u8?tsファイル?お任せあれ
ある日、旅の商人(AVAsset)が持ってきたのは、
あるURLにある .m3u8
ファイル(HLS形式のプレイリスト)。
その中には、こんな情報が書かれていた:
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=800000
https://example.com/video/low.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1600000
https://example.com/video/mid.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000
https://example.com/video/high.m3u8
それを見た参謀(AVPlayerItem)は言った

王様、このファイルには複数の画質(ビットレート)があります。
ユーザーの回線速度を見ながら最適な画質を自動選択します
そこに旅の商人(AVAsset)が胸を張って一歩前へ。

道案内ならこの私にお任せを。
メインの m3u8 から適切なバリアントを選び、
そこに並ぶ .ts セグメントを順に取り寄せましょう。
必要とあらばメタデータやトラック情報も拝見、
再生に必要な**キー情報(DRMがあればライセンス)**の手配も段取りいたします
王様(AVPlayer)は満足げにうなずき、こう命じた。

よし、じゃあ mid.m3u8
を選んで、今すぐ再生を開始せよ
こうして、王国では .ts
ファイル(動画のチャンク)が順番に読み込まれ、
視聴者の目にはスムーズな動画体験が届けられたとさ、ちゃんちゃん
(GPT-5で試してみたけどなんか微妙、、、ロールモデルが)
🎯 ポイント:
.m3u8
はプレイリスト(HLSの目次)- 実体は
.ts
ファイル(数秒単位の動画チャンク) - AVFoundationは**Adaptive Bitrate(ABR)**で最適な画質を選択
- AVPlayer + AVPlayerItem + AVAssetで協力プレイが行われてる
実際の流れはこんな感じ:
AVPlayerによるHLS再生の流れ
├─ 1. AVPlayerを初期化(m3u8のURLを渡す)
│ └─ 内部でAVURLAssetが生成される
│
├─ 2. AVURLAssetがm3u8を読み込む
│ ├─ プレイリスト(m3u8)を取得
│ ├─ #EXT-X-STREAM-INFの情報から適切なVariant Playlistを選択(ABR)
│ └─ 選ばれたVariantのm3u8を再度読み込む
│
├─ 3. メディアセグメント(.ts)の取得が始まる
│ ├─ Variant Playlistに定義された.tsファイルのURLを順次リクエスト
│ ├─ 一定数バッファリングされるまで再生開始されない(=ReadyToPlay)
│ └─ バッファ監視 → ネットワーク状態で画質自動変更(ABR)
│
├─ 4. AVPlayerItemが.tsを順にデコードしてAVPlayerに供給
│ ├─ デコードされたフレームをAVPlayerLayerまたはVideoPlayerが表示
│ ├─ 音声データはAudioQueueに送られスピーカー再生
│ └─ シークや一時停止等の操作はAVPlayerItem経由で制御
│
└─ 5. 再生終了 or ユーザー操作でセッション終了
├─ AVPlayerItemのstatusが.didPlayToEndTimeに到達
└─ 再生停止、リリース、または新しいAVPlayerItemで再開
1.2 最小構成で動画再生 (コードあり)
ここでは、最小構成でHLS動画を再生するサンプルを作って学んでみましょう。
import SwiftUI
import AVKit
struct VideoPlayerView: View {
private let player = AVPlayer(url: URL(string: "https://example.com/stream.m3u8")!)
var body: some View {
VideoPlayer(player: player)
.onAppear {
player.play()
}
.onDisappear {
player.pause()
}
}
}
🎯 ポイント:
AVPlayer(url:)
→.m3u8
URLからAVPlayer生成。自動でHLS認識してくれる!VideoPlayer(player:)
→ SwiftUI用のラッパー。iOS 14+で使える。onAppear / onDisappear
→ 再生制御をここで行えば、無駄な再生を防げる
1.3 再生状態の監視(KVO & Notification)
HLS再生やローカル動画でも、ユーザー体験を爆上げするには
「今どういう状態なのか?」をちゃんと把握するのが大事。
🎯 監視対象まとめ:
監視項目 | 説明 | 方法 |
---|---|---|
status | 再生準備OKかどうか | KVO |
rate | 再生中かどうか(0 = 停止中) | KVO |
timeControlStatus | 自動再生 or バッファ中か | KVO (iOS10+) |
isPlaybackBufferEmpty | バッファ切れか? | KVO |
isPlaybackLikelyToKeepUp | バッファ十分で再生継続できそうか? | KVO |
.AVPlayerItemDidPlayToEndTime | 再生完了通知 | Notification |
実装例:KVOとNotificationで監視:
import AVFoundation
import Combine
final class VideoPlayerObserver: NSObject {
private var player: AVPlayer!
private var playerItem: AVPlayerItem!
private var observers = Set<NSKeyValueObservation>()
private var cancellables = Set<AnyCancellable>()
init(url: URL) {
super.init()
self.playerItem = AVPlayerItem(url: url)
self.player = AVPlayer(playerItem: playerItem)
observePlayer()
}
private func observePlayer() {
// 再生準備
observers.insert(
playerItem.observe(\.status, options: [.new]) { item, _ in
print("🔍 status:", item.status.rawValue)
}
)
// バッファ状態
observers.insert(
playerItem.observe(\.isPlaybackBufferEmpty, options: [.new]) { item, _ in
print("⚠️ Buffer Empty:", item.isPlaybackBufferEmpty)
}
)
observers.insert(
playerItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { item, _ in
print("🚀 Likely To Keep Up:", item.isPlaybackLikelyToKeepUp)
}
)
// 再生状態(rate)
observers.insert(
player.observe(\.rate, options: [.new]) { player, _ in
print("🎮 Rate:", player.rate)
}
)
// 再生完了通知
NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: playerItem)
.sink { _ in
print("🏁 再生終了!")
}
.store(in: &cancellables)
}
}
rate == 1.0
→ 再生中rate == 0.0
でもtimeControlStatus == .waitingToPlayAtSpecifiedRate
ならバッファ中isPlaybackBufferEmpty == true
→ バッファ切れ確定、ローディングUI出すisPlaybackLikelyToKeepUp == true
→ 再開してもOKの合図
1.4 バッファ、シーク(探す)、ABRの基礎知識
バッファ
AVPlayerは、.ts
セグメントを先読みしてバッファに溜め込む。
isPlaybackBufferEmpty == true
: バッファ切れ、止まってるisPlaybackLikelyToKeepUp == false
: まだ溜まりきってないisPlaybackLikelyToKeepUp == true
: 再生してOK
シーク処理
HLSでのシークは一筋縄ではいかないので注意
- 指定された秒数に近い
.ts
セグメントを探す - そのセグメントからデコード再開
- 適切なIフレームまでジャンプ(※完璧なピンポイントじゃない)
player.seek(to: CMTime(seconds: 30, preferredTimescale: 600)) {
finished in
print("✅ Seek完了: \(finished)")
}
Seekのコツ:
.zero
からの初期シークは失敗しやすい(読み込み前)toleranceBefore / After
を調整して精度を上げられる- ピンポイントでサムネ取るときは
AVAssetImageGenerator
のほうが信頼性高い
ABR(Adaptive Bitrate Streaming)
AVFoundationはネットワーク状態を見て自動で画質を変えられる
- 回線が弱い → 低ビットレート
.m3u8
を選択 - 回線が回復 → 高ビットレートに切り替え(自動的に)
この動きは完全に内部処理されるから、基本はノータッチでOK。
ただし preferredPeakBitRate
を設定すれば、上限を指定できる👇
player.currentItem?.preferredPeakBitRate = 1_500_000 // 1.5 Mbps
次回: 【録音・録画】AVCaptureの基本
