[Xcode/Swift] AVFoundation完全理解への道② (AVPlayer)

この記事で学ぶこと:

AVPlayerによる動画再生

①を読んでなくても問題ないですが、概念が曖昧というか方は①から読まれるのをおすすめします。👇

[Xcode/Swift] AVFoundation完全理解への道①

AVPlayerによる動画再生

1.1 HLS (m3u8) 再生の仕組みをストーリー式で理解してみる (RPG)

(GPTに考えてもらいました)

ある日、AVPlayer王国に暮らす3人のキャラたちが集まった..

  • 👑 AVPlayer(王様)
  • 🧙‍♂️ AVPlayerItem(参謀)
  • 🧳 AVAsset(旅の商人)
王様
(AVPlayer)
王様 (AVPlayer)

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

参謀
(AVPlayerItem)
参謀 (AVPlayerItem)

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

旅の商人
(AVAsset)
旅の商人 (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)は言った

参謀
(AVPlayerItem)
参謀 (AVPlayerItem)

王様、このファイルには複数の画質(ビットレート)があります。

ユーザーの回線速度を見ながら最適な画質を自動選択します

そこに旅の商人(AVAsset)が胸を張って一歩前へ。

旅の商人
(AVAsset)
旅の商人 (AVAsset)

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

王様(AVPlayer)は満足げにうなずき、こう命じた。

王様
(AVPlayer)
王様 (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でのシークは一筋縄ではいかないので注意

  1. 指定された秒数に近い .ts セグメントを探す
  2. そのセグメントからデコード再開
  3. 適切な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の基本

[Xcode/Swift] AVFoundation完全理解への道③ (AVCapture)