[Xcode/Swift] AVFoundation完全理解への道⑥ (実装Tips集)

この記事で学ぶこと:

プロジェクトで使えるTips集

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

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

プロジェクトで使えるTips集

1.1 カスタムプレイヤーUIを作るには?

VideoPlayer(SwiftUI純正)でも簡単再生はできるけど、
カスタムUI(再生ボタン・スライダー・時間表示など)を作りたいなら、AVPlayer直操作がマスト

import SwiftUI
import AVFoundation
import UIKit
import AVKit

struct CustomPlayerView: View {
    @State private var player = AVPlayer(url: URL(string: "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8")!)
    @State private var isPlaying = false

    var body: some View {
        VStack {
            VideoPlayerContainer(player: player)
                .frame(height: 250)

            HStack {
                Button(action: {
                    isPlaying ? player.pause() : player.play()
                    isPlaying.toggle()
                }) {
                    Image(systemName: isPlaying ? "pause.fill" : "play.fill")
                        .font(.largeTitle)
                }

                Spacer()

                Button("⏹ Stop") {
                    player.pause()
                    player.seek(to: .zero)
                    isPlaying = false
                }
            }
            .padding()
        }
        .onDisappear {
            player.pause()
        }
    }
}

struct VideoPlayerContainer: UIViewControllerRepresentable {
    let player: AVPlayer

    func makeUIViewController(context: Context) -> AVPlayerViewController {
        let controller = AVPlayerViewController()
        controller.player = player
        controller.showsPlaybackControls = false // カスタムUIにするなら必須
        return controller
    }

    func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {}
}

ポイント:

項目ポイント
AVPlayerViewControllerUIKitから持ってきて再生処理担当
showsPlaybackControls = false純正UIを非表示にして、SwiftUI側で制御
@State isPlayingUIと連動させてトグル再生を実現

1.2 サムネイル取得 (AVAssetImageGenerator)

最小構成:

func generateThumbnail(from url: URL, at seconds: Double) async throws -> UIImage {
    let asset = AVAsset(url: url)
    let generator = AVAssetImageGenerator(asset: asset)
    generator.appliesPreferredTrackTransform = true

    let time = CMTime(seconds: seconds, preferredTimescale: 600)
    let cgImage = try generator.copyCGImage(at: time, actualTime: nil)
    return UIImage(cgImage: cgImage)
}

ポイント:

項目ポイント
.appliesPreferredTrackTransform動画の回転補正を自動でやってくれる
copyCGImage一発でCGImage取得。非同期にするなら generateCGImagesAsynchronously
.toleranceBefore/After精度を高めたいときのオプション(省略可)

1.3 音だけ再生 / 映像だけ再生

🎧 音だけ再生

let playerItem = AVPlayerItem(url: audioURL)
playerItem.videoComposition = AVVideoComposition() // 空のビデオで映像オフ
let player = AVPlayer(playerItem: playerItem)

🎬 映像だけ再生(音なし)

player.isMuted = true

ポイント:

モード方法
音だけAVAudioPlayerでもOK、または videoComposition を空に
映像だけplayer.isMuted = true or AVMutableAudioMix でミュート制御

1.4 SwiftUI + AVFoundation の組み合わせ例

カメラプレビューをSwiftUIに表示

struct CameraPreviewView: UIViewRepresentable {
    let session: AVCaptureSession

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        let previewLayer = AVCaptureVideoPreviewLayer(session: session)
        previewLayer.videoGravity = .resizeAspectFill
        previewLayer.frame = UIScreen.main.bounds
        view.layer.addSublayer(previewLayer)
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {}
}

ポイント:

項目ポイント
UIViewRepresentableSwiftUIとUIKitをブリッジする技
AVCaptureVideoPreviewLayerセッションの映像をリアルタイムで表示
videoGravityアスペクト比調整。resizeAspectFill or resizeAspect

Q&Aまとめ

Q1. 「そもそも再生できない、、、」

💥 よくある原因:

原因チェックポイント
.m3u8 or .mp4 のURLが無効URL(string:) → nilになってないか?
ネットが切れてるオフラインでHLSは無理(ローカルキャッシュしてなければ)
AVPlayerItemのstatusがfailedKVOで監視して原因ログを出すべし
Info.plistでATS制限HTTP URLなら NSAppTransportSecurity を緩める必要あり
動画の形式が非対応.webm や非H.264だとアウト

チェック:

player.currentItem?.observe(\.status, options: [.new]) { item, _ in
    switch item.status {
    case .readyToPlay:
        print("🎉 Ready to play!")
    case .failed:
        print("❌ Failed:", item.error ?? "unknown error")
    default:
        break
    }
}

HLSの.m3u8再生はネット接続 + 有効なURLがマスト


Q2. 「録音したデータを保存するには?」

🎤 基本の録音構成:

  • AVAudioRecorder を使う
  • 保存先は .m4a(AAC形式)あたりが現実的
  • prepareToRecord()record()stop() の流れ

サンプル:

import AVFoundation

let audioSession = AVAudioSession.sharedInstance()
try? audioSession.setCategory(.playAndRecord, mode: .default)
try? audioSession.setActive(true)

let url = FileManager.default.temporaryDirectory.appendingPathComponent("record.m4a")
let settings: [String: Any] = [
    AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
    AVSampleRateKey: 12000,
    AVNumberOfChannelsKey: 1,
    AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]

let recorder = try AVAudioRecorder(url: url, settings: settings)
recorder.record()

// 停止時
recorder.stop()

保存後は url.path をログ出して、共有や再生で使えるようにしておくとGOOD


Q3. 「なぜシークが正確ではないのか」

A.

原因内容
シークは1フレーム単位で動くフレームの種類の都合上、完全なピンポイントにはならない
HLSは.ts単位で飛ぶtsが3秒単位とかなので、30秒指定しても29.2秒に着地したりする
toleranceBefore/Afterの設定が甘い許容誤差がデフォルトだと広くてズレやすい

精度を高めたいなら:

// ⚠️ .zero にするとシビアすぎて逆に失敗することもあるので注意
let time = CMTime(seconds: 30.0, preferredTimescale: 600)
player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)

// ピンポイントの静止画像が欲しい時はAVAssetImageGeneratorを使用
let asset = AVAsset(url: yourURL)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
let image = try generator.copyCGImage(at: time, actualTime: nil)

まとめ

これでAVFoundationのまとめは終了、GPT君にも記事構成を考えてもらったり、自分も多く学びになりました。

コードはやはり手を動かして学ぶことが一番、座学だけでなく座学 + アウトプットでスキルを高めていきましょう。