版本記錄
版本號 | 時間 |
---|---|
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()
}
}
後記
本篇主要講述了基於
AVKit
和AVFoundation
框架的視頻流App的構建,感興趣的給個贊或者關注~~~