この記事で学ぶこと:
AVFoundation内部処理雑学編 (実装で完璧に知っておかなくてもいいけど知っておくとGood的な)
①を読んでなくても問題ないですが、概念が曖昧というか方は①から読まれるのをおすすめします。👇

Contents 非表示
AVFoundation内部処理雑学編
1.1 m3u8からの.ts取得フロー
AVPlayerでHLS再生(.m3u8)する流れ
├─ AVPlayerにAVPlayerItemをセット
│ └─ AVPlayerItemがAVURLAssetを参照
│ └─ AVURLAssetが.m3u8ファイル(Master Playlist)を読み込む
│ ├─ #EXT-X-STREAM-INF: 各画質(m3u8)のURLを取得
│ └─ ネット速度に応じた Variant Playlist を選択(ABR)
│ └─ 選択された m3u8 から .ts チャンクの一覧を取得
│ ├─ .ts(MPEG-TS)ファイルを順次ダウンロード
│ └─ 再生バッファに投入 → AVPlayerでデコード・再生
ポイント:
項目 | 内容 | 覚えるべきポイント |
---|---|---|
.m3u8 | HLSのプレイリスト(Master or Variant) | .ts の目次みたいなもん |
AVAsset | URLから .m3u8 を読み込み、AVPlayerItemに渡す | 情報取得や準備に使われる裏方 |
ABR | Adaptive Bitrate Streaming、自動で画質選択 | プレイヤーが回線に応じて .m3u8 を切り替える |
.ts チャンク | 3〜6秒程度の小さな動画ファイル | 順次ダウンロード・再生される |
再生準備 | .ts がある程度読み込まれると再生可能に | AVPlayerItem.status == .readyToPlay で判定 |
サンプル:
import SwiftUI
import AVKit
struct HLSPlayerView: View {
private let url = URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!
private let player = AVPlayer()
var body: some View {
VideoPlayer(player: player)
.onAppear {
let item = AVPlayerItem(url: url)
player.replaceCurrentItem(with: item)
player.play()
}
.onDisappear {
player.pause()
}
.navigationTitle("🎭 m3u8再生")
.navigationBarTitleDisplayMode(.inline)
}
}
1.2 再生中にネットが切れた時の内部処理
AVPlayer 再生中にネット切断 → 自動停止の流れ
├─ .ts セグメントの取得に失敗
│ └─ AVPlayerItemの isPlaybackBufferEmpty = true になる
│ ├─ 再生一時停止(rate = 0)
│ └─ KVOで状態検知 → ローディングUIなど対応可能
├─ ネット回復後
│ └─ バッファが再び溜まると isPlaybackLikelyToKeepUp = true
│ └─ AVPlayerが自動再開(または明示的に再開)
ポイント:
項目 | 内容 | 覚えるべきポイント |
---|---|---|
バッファ切れ | .ts が取得できず再生ストップ | isPlaybackBufferEmpty == true |
自動停止 | AVPlayerが rate = 0 に落ちる | ユーザー操作じゃなくても止まる |
バッファ回復 | ネット回復後に .ts が再取得される | isPlaybackLikelyToKeepUp == true で再生再開可能 |
KVO監視 | 状態をリアルタイムで監視 | UIでローディング表示する条件になる |
手動再開 | プレイヤーが勝手に再生しないこともある | player.play() で明示的に再開しよう |
サンプル:
import SwiftUI
import AVKit
import Combine
final class BufferMonitor: ObservableObject {
@Published var isBuffering = false
private var playerItemObservation: NSKeyValueObservation?
func observe(item: AVPlayerItem) {
playerItemObservation = item.observe(\.isPlaybackBufferEmpty, options: [.new]) { [weak self] item, _ in
DispatchQueue.main.async {
self?.isBuffering = item.isPlaybackBufferEmpty
}
}
}
}
struct BufferingPlayerView: View {
private let url = URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!
@StateObject private var bufferMonitor = BufferMonitor()
private let player = AVPlayer()
var body: some View {
ZStack {
VideoPlayer(player: player)
.onAppear {
let item = AVPlayerItem(url: url)
bufferMonitor.observe(item: item)
player.replaceCurrentItem(with: item)
player.play()
}
if bufferMonitor.isBuffering {
VStack {
ProgressView("Buffering...😵💫")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.black.opacity(0.5))
}
}
.navigationTitle("📶 バッファ物語")
.navigationBarTitleDisplayMode(.inline)
}
}
1.3 シークとサムネイルの裏側(YouTube風)
AVPlayerでseek(to:)した時の内部処理
├─ 指定されたCMTimeに最も近い.tsを探す
│ └─ Iフレーム(キーフレーム)までジャンプ
│ └─ シークは数秒ズレることもある(ピンポイント不可)
│
└─ サムネイル取得の流れ(AVAssetImageGenerator)
├─ AVAssetを元にイメージジェネレータ生成
├─ 指定時間のフレームを探す(キーフレーム基準)
└─ CGImage生成 → UIImage化して表示
ポイント:
項目 | 内容 | 覚えるべきポイント |
---|---|---|
シーク処理 | Iフレーム単位でしか移動できない | 数秒ズレるのは仕様 |
CMTime指定 | 秒数からCMTimeを生成してシーク | preferredTimescale = 600 が安定 |
tolerance設定 | 精度を高めるための範囲指定 | .zero 指定でピンポイント(ただし成功率下がる) |
AVAssetImageGenerator | 任意時間のフレーム画像を抽出 | ローカル動画に向いてる(HLSは不安定) |
よくある失敗 | duration未読み込み、HLS、範囲外時間 | loadValuesAsynchronously + durationチェックで回避 |
サンプル:
import SwiftUI
import AVFoundation
struct ThumbnailSeekerView: View {
let asset = AVAsset(url: URL(string: "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8")!)
@State private var image: UIImage?
var body: some View {
VStack(spacing: 20) {
if let img = image {
Image(uiImage: img)
.resizable()
.scaledToFit()
.frame(height: 200)
} else {
ProgressView("Loading Thumbnail...")
}
Button("📸 30秒のサムネを取得") {
generateThumbnail(at: 30)
}
}
.padding()
.navigationTitle("🔍 シーク&サムネ")
.navigationBarTitleDisplayMode(.inline)
}
func generateThumbnail(at seconds: Double) {
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
let time = CMTime(seconds: seconds, preferredTimescale: 600)
DispatchQueue.global().async {
do {
let cgImage = try generator.copyCGImage(at: time, actualTime: nil)
DispatchQueue.main.async {
self.image = UIImage(cgImage: cgImage)
}
} catch {
print("❌ Thumbnail error:", error)
}
}
}
}
次回: プロジェクトで使えるTips集
