AVKit框架詳細解析(五) —— 基於AVKit 和 AVFoundation框架的視頻流App的構建(二) 版本記錄 前言 源碼 後記

版本記錄

版本號 時間
V1.0 2021.08.15 星期日

前言

AVKit框架爲媒體播放創建視圖級別的服務,包含用戶控件,章節導航以及對字幕和隱藏式字幕的支持。接下來幾篇我們就一起看一下這個框架。感興趣的可以看下面幾篇文章。
1. AVKit框架詳細解析(一) —— 基本概覽(一)
2. AVKit框架詳細解析(二) —— 基於視頻播放器的畫中畫實現(一)
3. AVKit框架詳細解析(三) —— 基於視頻播放器的畫中畫實現(二)
4. AVKit框架詳細解析(四) —— 基於AVKit 和 AVFoundation框架的視頻流App的構建(一)

源碼

1. Swift

首先看下工程組織結構

下面就是源碼了

1. AppMain.swift
import SwiftUI
import AVFoundation

@main
struct AppMain: App {
  init() {
    setVideoPlaybackCategory()
  }

  var body: some Scene {
    WindowGroup {
      VideoFeedView()
    }
  }

  private func setMixWithOthersPlaybackCategory() {
    try? AVAudioSession.sharedInstance().setCategory(
      AVAudioSession.Category.ambient,
      mode: AVAudioSession.Mode.moviePlayback,
      options: [.mixWithOthers])
  }

  private func setVideoPlaybackCategory() {
    try? AVAudioSession.sharedInstance().setCategory(.playback)
  }
}
2. Video.swift
import Foundation

struct Video: Decodable, Identifiable {
  let id = UUID()
  let title: String
  let fileName: String
  let subtitle: String
  let remoteVideoURL: URL?

  private enum CodingKeys: String, CodingKey {
    case title, subtitle
    case fileName = "file_name"
    case remoteVideoURL = "remote_video_url"
  }
}

extension Video {
  var localVideoURL: URL? {
    return Bundle.main.url(forResource: fileName, withExtension: "mp4")
  }

  var videoURL: URL? {
    return remoteVideoURL ?? localVideoURL
  }
}

extension Video {
  static func fetchLocalVideos() -> [Video] {
    return readJSON(fileName: "LocalVideos")
  }

  static func fetchRemoteVideos() -> [Video] {
    return readJSON(fileName: "RemoteVideos")
  }
}
3. VideoClip.swift
import Foundation

struct VideoClip: Decodable {
  let fileName: String

  private enum CodingKeys: String, CodingKey {
    case fileName = "file_name"
  }
}

extension VideoClip {
  static var urls: [URL] {
    return VideoClip.fetchLocalVideos().compactMap {
      Bundle.main.url(forResource: $0.fileName, withExtension: "mp4")
    }
  }
}

extension VideoClip {
  static func fetchLocalVideos() -> [VideoClip] {
    return readJSON(fileName: "LocalVideoClips")
  }
}
4. Utils.swift
import Foundation

func readJSON<T: Decodable>(fileName: String) -> [T] {
  if let url = Bundle.main.url(forResource: fileName, withExtension: "json") {
    do {
      let data = try Data(contentsOf: url)
      return try JSONDecoder().decode([T].self, from: data)
    } catch {
      print("Failed decoding JSON file: \(fileName).")
      return []
    }
  }
  return []
}
5. LoopingPlayerView.swift
import SwiftUI
import AVKit

struct LoopingPlayerView: UIViewRepresentable {
  class Coordinator: NSObject, AVPlayerViewControllerDelegate, AVPictureInPictureControllerDelegate {
    private let parent: LoopingPlayerView

    var pipController: AVPictureInPictureController? {
      didSet {
        pipController?.delegate = self
      }
    }

    init(_ parent: LoopingPlayerView) {
      self.parent = parent
    }

    func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
      parent.shouldOpenPiP = false
      completionHandler(true)
    }
  }

  let videoURLs: [URL]

  @Binding var rate: Float
  @Binding var volume: Float
  @Binding var shouldOpenPiP: Bool

  func updateUIView(_ uiView: LoopingPlayerUIView, context: Context) {
    uiView.setVolume(volume)
    uiView.setRate(rate)
    if shouldOpenPiP && context.coordinator.pipController?.isPictureInPictureActive == false {
      context.coordinator.pipController?.startPictureInPicture()
    } else if !shouldOpenPiP && context.coordinator.pipController?.isPictureInPictureActive == true {
      context.coordinator.pipController?.stopPictureInPicture()
    }
  }

  func makeUIView(context: Context) -> LoopingPlayerUIView {
    let view = LoopingPlayerUIView(urls: videoURLs)
    view.setVolume(volume)
    view.setRate(rate)

    context.coordinator.pipController = AVPictureInPictureController(playerLayer: view.playerLayer)

    return view
  }

  static func dismantleUIView(_ uiView: LoopingPlayerUIView, coordinator: ()) {
    uiView.cleanup()
  }

  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
}

final class LoopingPlayerUIView: UIView {
  private var player: AVQueuePlayer?
  private var token: NSKeyValueObservation?

  private var allURLs: [URL]

  var playerLayer: AVPlayerLayer {
    // swiftlint:disable:next force_cast
    layer as! AVPlayerLayer
  }

  override class var layerClass: AnyClass {
    return AVPlayerLayer.self
  }

  init(urls: [URL]) {
    allURLs = urls
    player = AVQueuePlayer()

    super.init(frame: .zero)

    addAllVideosToPlayer()

    playerLayer.player = player

    token = player?.observe(\.currentItem) { [weak self] player, _ in
      if player.items().count == 1 {
        self?.addAllVideosToPlayer()
      }
    }
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  private func addAllVideosToPlayer() {
    for url in allURLs {
      let asset = AVURLAsset(url: url)
      let item = AVPlayerItem(asset: asset)
      player?.insert(item, after: player?.items().last)
    }
  }

  func setVolume(_ value: Float) {
    player?.volume = value
  }

  func setRate(_ value: Float) {
    player?.rate = value
  }

  func cleanup() {
    player?.pause()
    player?.removeAllItems()
    player = nil
  }
}
6. VideoPlayerView.swift
import SwiftUI
import AVKit

struct VideoPlayerView: UIViewControllerRepresentable {
  let player: AVPlayer?

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

  func makeUIViewController(context: Context) -> AVPlayerViewController {
    let controller = AVPlayerViewController()
    controller.player = player
    return controller
  }
}`
7. VideoFeedView.swift
import SwiftUI
import AVKit

struct VideoFeedView: View {
  private let videos = Video.fetchLocalVideos() + Video.fetchRemoteVideos()
  private let videoClips = VideoClip.urls

  @State private var selectedVideo: Video?

  @State private var embeddedVideoRate: Float = 0.0
  @State private var embeddedVideoVolume: Float = 0.0
  @State private var shouldShowEmbeddedVideoInPiP = false

  var body: some View {
    NavigationView {
      List {
        makeEmbeddedVideoPlayer()
        ForEach(videos) { video in
          Button {
            selectedVideo = video
          } label: {
            VideoRow(video: video)
          }
        }
      }
      .navigationTitle("Travel Vlogs")
    }
    .fullScreenCover(item: $selectedVideo) {
      embeddedVideoRate = 1.0
    } content: { item in
      makeFullScreenVideoPlayer(for: item)
    }
  }

  @ViewBuilder
  private func makeFullScreenVideoPlayer(for video: Video) -> some View {
    if let url = video.videoURL {
      let avPlayer = AVPlayer(url: url)

      VideoPlayerView(player: avPlayer)
        .edgesIgnoringSafeArea(.all)
        .onAppear {
          embeddedVideoRate = 0.0
          avPlayer.play()
        }
    } else {
      ErrorView()
    }
  }

  private func makeEmbeddedVideoPlayer() -> some View {
    HStack {
      Spacer()
      
      LoopingPlayerView(
        videoURLs: videoClips,
        rate: $embeddedVideoRate,
        volume: $embeddedVideoVolume,
        shouldOpenPiP: $shouldShowEmbeddedVideoInPiP)
        .background(Color.black)
        .frame(width: 250, height: 140)
        .cornerRadius(8)
        .shadow(radius: 4)
        .padding(.vertical)
        .onAppear {
          embeddedVideoRate = 1
        }
        .onTapGesture(count: 2) {
          embeddedVideoRate = embeddedVideoRate == 1.0 ? 2.0 : 1.0
        }
        .onTapGesture {
          embeddedVideoVolume = embeddedVideoVolume == 1.0 ? 0.0 : 1.0
        }
        .onLongPressGesture {
          shouldShowEmbeddedVideoInPiP.toggle()
        }
      
      Spacer()
    }
  }
}

struct VideoFeedView_Previews: PreviewProvider {
  static var previews: some View {
    VideoFeedView()
  }
}
8. VideoRow.swift
import SwiftUI

struct VideoRow: View {
  let video: Video

  private let imageHeight: CGFloat = 250
  private let imageCornerRadius: CGFloat = 12.0

  var body: some View {
    VStack(alignment: .leading) {
      Text(video.title)
        .font(.title2)

      GeometryReader { proxy in
        Image("\(video.fileName)")
          .resizable()
          .scaledToFill()
          .frame(width: proxy.size.width, height: imageHeight)
          .clipShape(
            RoundedRectangle(
              cornerRadius: imageCornerRadius,
              style: .continuous))
          .shadow(radius: imageCornerRadius / 3.0)
      }
      .frame(height: imageHeight)

      Text(video.subtitle)
        .font(.subheadline)
    }
    .padding(.vertical)
  }
}

struct VideoRow_Previews: PreviewProvider {
  static var previews: some View {
    VideoRow(video: Video.fetchLocalVideos()[0])
  }
}
9. ErrorView.swift
import SwiftUI

struct ErrorView: View {
  @Environment(\.presentationMode) var presentationMode

  var body: some View {
    VStack {
      Text("What do you think could go wrong? 🤔")
        .font(.title3)
        .padding()
      Button {
        presentationMode.wrappedValue.dismiss()
      }
      label: {
        Text("Dismiss")
          .font(.title3)
          .bold()
      }
    }
  }
}

struct ErrorView_Previews: PreviewProvider {
  static var previews: some View {
    ErrorView()
  }
}

後記

本篇主要講述了基於AVKitAVFoundation框架的視頻流App的構建,感興趣的給個贊或者關注~~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章