この記事で学ぶこと:
プロジェクトで使えるTips集
①を読んでなくても問題ないですが、概念が曖昧というか方は①から読まれるのをおすすめします。👇
 [Xcode/Swift] AVFoundation完全理解への道①
[Xcode/Swift] AVFoundation完全理解への道①
Contents 非表示
プロジェクトで使える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) {}
}ポイント:
| 項目 | ポイント | 
|---|---|
| AVPlayerViewController | UIKitから持ってきて再生処理担当 | 
| showsPlaybackControls = false | 純正UIを非表示にして、SwiftUI側で制御 | 
| @State isPlaying | UIと連動させてトグル再生を実現 | 
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 = trueorAVMutableAudioMixでミュート制御 | 
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) {}
}ポイント:
| 項目 | ポイント | 
|---|---|
| UIViewRepresentable | SwiftUIとUIKitをブリッジする技 | 
| AVCaptureVideoPreviewLayer | セッションの映像をリアルタイムで表示 | 
| videoGravity | アスペクト比調整。 resizeAspectFillorresizeAspect | 
Q&Aまとめ
Q1. 「そもそも再生できない、、、」
💥 よくある原因:
| 原因 | チェックポイント | 
|---|---|
| .m3u8or.mp4のURLが無効 | URL(string:)→ nilになってないか? | 
| ネットが切れてる | オフラインでHLSは無理(ローカルキャッシュしてなければ) | 
| AVPlayerItemのstatusが failed | KVOで監視して原因ログを出すべし | 
| 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君にも記事構成を考えてもらったり、自分も多く学びになりました。
コードはやはり手を動かして学ぶことが一番、座学だけでなく座学 + アウトプットでスキルを高めていきましょう。

