Improve building AVPlayer composition

This commit is contained in:
Arkadiusz Fal 2021-06-18 12:17:01 +02:00
parent d551dee426
commit c40fc3e042
5 changed files with 114 additions and 55 deletions

View File

@ -18,7 +18,7 @@ struct PlayerViewController: UIViewControllerRepresentable {
} }
fileprivate func loadStream(_ stream: Stream?, loadBest: Bool = false) { fileprivate func loadStream(_ stream: Stream?, loadBest: Bool = false) {
if stream != state.streamToLoad { if stream != state.nextStream {
state.loadStream(stream) state.loadStream(stream)
addTracksAndLoadAssets(stream!, loadBest: loadBest) addTracksAndLoadAssets(stream!, loadBest: loadBest)
} }
@ -34,31 +34,6 @@ struct PlayerViewController: UIViewControllerRepresentable {
} }
} }
fileprivate func addTrack(_ asset: AVURLAsset, stream: Stream, type: AVMediaType? = nil) {
let types: [AVMediaType] = stream.type == .adaptive ? [type!] : [.video, .audio]
types.forEach { type in
guard let assetTrack = asset.tracks(withMediaType: type).first else {
return
}
if let track = state.composition.tracks(withMediaType: type).first {
logger.info("removing \(type) track")
state.composition.removeTrack(track)
}
let track = state.composition.addMutableTrack(withMediaType: type, preferredTrackID: kCMPersistentTrackID_Invalid)!
try! track.insertTimeRange(
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1)),
of: assetTrack,
at: .zero
)
logger.info("inserted \(type) track")
}
}
fileprivate func handleAssetLoad(_ stream: Stream, type: AVMediaType, loadBest: Bool = false) { fileprivate func handleAssetLoad(_ stream: Stream, type: AVMediaType, loadBest: Bool = false) {
logger.info("handling asset load: \(stream.type), \(stream.description)") logger.info("handling asset load: \(stream.type), \(stream.description)")
@ -84,6 +59,12 @@ struct PlayerViewController: UIViewControllerRepresentable {
} }
} }
fileprivate func addTrack(_ asset: AVURLAsset, stream: Stream, type: AVMediaType? = nil) {
let types: [AVMediaType] = stream.type == .adaptive ? [type!] : [.video, .audio]
types.forEach { state.addTrackToNextComposition(asset, type: $0) }
}
fileprivate func loadBestStream() { fileprivate func loadBestStream() {
guard state.currentStream != video.bestStream else { guard state.currentStream != video.bestStream else {
return return
@ -108,7 +89,7 @@ struct PlayerViewController: UIViewControllerRepresentable {
func updateUIViewController(_ controller: StreamAVPlayerViewController, context _: Context) { func updateUIViewController(_ controller: StreamAVPlayerViewController, context _: Context) {
var items: [UIMenuElement] = [] var items: [UIMenuElement] = []
if state.streamToLoad != nil { if state.nextStream != nil {
items.append(actionsMenu) items.append(actionsMenu)
} }
@ -116,7 +97,6 @@ struct PlayerViewController: UIViewControllerRepresentable {
#if os(tvOS) #if os(tvOS)
controller.transportBarCustomMenuItems = items controller.transportBarCustomMenuItems = items
#endif
if let skip = skipSegmentAction { if let skip = skipSegmentAction {
if controller.contextualActions.isEmpty { if controller.contextualActions.isEmpty {
@ -125,6 +105,7 @@ struct PlayerViewController: UIViewControllerRepresentable {
} else { } else {
controller.contextualActions = [] controller.contextualActions = []
} }
#endif
} }
fileprivate var streamingQualityMenu: UIMenu { fileprivate var streamingQualityMenu: UIMenu {
@ -150,10 +131,10 @@ struct PlayerViewController: UIViewControllerRepresentable {
} }
fileprivate var cancelLoadingAction: UIAction { fileprivate var cancelLoadingAction: UIAction {
UIAction(title: "Cancel loading \(state.streamToLoad.description) stream") { _ in UIAction(title: "Cancel loading \(state.nextStream.description) stream") { _ in
DispatchQueue.main.async { DispatchQueue.main.async {
state.streamToLoad.cancelLoadingAssets() state.nextStream.cancelLoadingAssets()
state.cancelLoadingStream(state.streamToLoad) state.cancelLoadingStream(state.nextStream)
} }
} }
} }

View File

@ -1,6 +1,7 @@
import AVFoundation import AVFoundation
import Foundation import Foundation
import Logging import Logging
import UIKit
final class PlayerState: ObservableObject { final class PlayerState: ObservableObject {
let logger = Logger(label: "net.arekf.Pearvidious.ps") let logger = Logger(label: "net.arekf.Pearvidious.ps")
@ -8,24 +9,40 @@ final class PlayerState: ObservableObject {
var video: Video var video: Video
@Published private(set) var player: AVPlayer! = AVPlayer() @Published private(set) var player: AVPlayer! = AVPlayer()
private(set) var composition = AVMutableComposition() @Published private(set) var composition = AVMutableComposition()
@Published private(set) var nextComposition = AVMutableComposition()
private var comp: AVMutableComposition?
@Published private(set) var currentStream: Stream! @Published private(set) var currentStream: Stream!
@Published private(set) var streamToLoad: Stream! @Published private(set) var nextStream: Stream!
@Published private(set) var streamLoading = false @Published private(set) var streamLoading = false
@Published private(set) var currentTime: CMTime? @Published private(set) var currentTime: CMTime?
@Published private(set) var savedTime: CMTime? @Published private(set) var savedTime: CMTime?
@Published var currentSegment: Segment? @Published var currentSegment: Segment?
var playerItem: AVPlayerItem { var playerItem: AVPlayerItem {
let playerItem = AVPlayerItem(asset: composition) let playerItem = AVPlayerItem(asset: composition)
playerItem.externalMetadata = [ var externalMetadata = [
makeMetadataItem(.commonIdentifierTitle, value: video.title), makeMetadataItem(.commonIdentifierTitle, value: video.title),
makeMetadataItem(.quickTimeMetadataGenre, value: video.genre), makeMetadataItem(.quickTimeMetadataGenre, value: video.genre),
makeMetadataItem(.commonIdentifierDescription, value: video.description) makeMetadataItem(.commonIdentifierDescription, value: video.description),
] ]
if let thumbnailData = try? Data(contentsOf: video.thumbnailURL!),
let image = UIImage(data: thumbnailData),
let pngData = image.pngData()
{
let artworkItem = makeMetadataItem(.commonIdentifierArtwork, value: pngData)
externalMetadata.append(artworkItem)
}
playerItem.externalMetadata = externalMetadata
playerItem.preferredForwardBufferDuration = 10 playerItem.preferredForwardBufferDuration = 10
return playerItem return playerItem
@ -46,17 +63,18 @@ final class PlayerState: ObservableObject {
} }
func loadStream(_ stream: Stream?) { func loadStream(_ stream: Stream?) {
guard streamToLoad != stream else { guard nextStream != stream else {
return return
} }
streamToLoad?.cancelLoadingAssets() nextStream?.cancelLoadingAssets()
removeTracksFromNextComposition()
DispatchQueue.main.async { DispatchQueue.main.async {
self.streamLoading = true self.streamLoading = true
self.streamToLoad = stream self.nextStream = stream
} }
logger.info("replace streamToLoad: \(streamToLoad?.description ?? "nil"), streamLoading \(streamLoading)") logger.info("replace streamToLoad: \(nextStream?.description ?? "nil"), streamLoading \(streamLoading)")
} }
func streamDidLoad(_ stream: Stream?) { func streamDidLoad(_ stream: Stream?) {
@ -64,24 +82,24 @@ final class PlayerState: ObservableObject {
currentStream?.cancelLoadingAssets() currentStream?.cancelLoadingAssets()
currentStream = stream currentStream = stream
streamLoading = streamToLoad != stream streamLoading = nextStream != stream
if streamToLoad == stream { if nextStream == stream {
streamToLoad = nil nextStream = nil
} }
addTimeObserver() addTimeObserver()
} }
func cancelLoadingStream(_ stream: Stream) { func cancelLoadingStream(_ stream: Stream) {
guard streamToLoad == stream else { guard nextStream == stream else {
return return
} }
streamToLoad = nil nextStream = nil
streamLoading = false streamLoading = false
logger.info("cancel streamToLoad: \(streamToLoad?.description ?? "nil"), streamLoading \(streamLoading)") logger.info("cancel streamToLoad: \(nextStream?.description ?? "nil"), streamLoading \(streamLoading)")
} }
func playStream(_ stream: Stream) { func playStream(_ stream: Stream) {
@ -92,6 +110,7 @@ final class PlayerState: ObservableObject {
logger.warning("loading \(stream.description) to player") logger.warning("loading \(stream.description) to player")
saveTime() saveTime()
replaceCompositionTracks()
player.replaceCurrentItem(with: playerItem) player.replaceCurrentItem(with: playerItem)
streamDidLoad(stream) streamDidLoad(stream)
@ -99,6 +118,47 @@ final class PlayerState: ObservableObject {
seekToSavedTime() seekToSavedTime()
} }
func addTrackToNextComposition(_ asset: AVURLAsset, type: AVMediaType) {
guard let assetTrack = asset.tracks(withMediaType: type).first else {
return
}
if let track = nextComposition.tracks(withMediaType: type).first {
logger.info("removing \(type) track")
nextComposition.removeTrack(track)
}
let track = nextComposition.addMutableTrack(withMediaType: type, preferredTrackID: kCMPersistentTrackID_Invalid)!
try! track.insertTimeRange(
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1000)),
of: assetTrack,
at: .zero
)
logger.info("inserted \(type) track")
}
func replaceCompositionTracks() {
logger.warning("replacing compositions")
composition = AVMutableComposition()
nextComposition.tracks.forEach { track in
let newTrack = composition.addMutableTrack(withMediaType: track.mediaType, preferredTrackID: kCMPersistentTrackID_Invalid)!
try? newTrack.insertTimeRange(
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1000)),
of: track,
at: .zero
)
}
}
func removeTracksFromNextComposition() {
nextComposition.tracks.forEach { nextComposition.removeTrack($0) }
}
func saveTime() { func saveTime() {
guard player != nil else { guard player != nil else {
return return
@ -131,7 +191,7 @@ final class PlayerState: ObservableObject {
player.currentItem?.tracks.forEach { $0.assetTrack?.asset?.cancelLoading() } player.currentItem?.tracks.forEach { $0.assetTrack?.asset?.cancelLoading() }
currentStream?.cancelLoadingAssets() currentStream?.cancelLoadingAssets()
streamToLoad?.cancelLoadingAssets() nextStream?.cancelLoadingAssets()
player.cancelPendingPrerolls() player.cancelPendingPrerolls()
player.replaceCurrentItem(with: nil) player.replaceCurrentItem(with: nil)

View File

@ -9,6 +9,18 @@ class Segment: ObservableObject, Hashable {
let uuid: String let uuid: String
let videoDuration: Int let videoDuration: Int
var start: Double {
segment.first!
}
var end: Double {
segment.last!
}
var duration: Double {
end - start
}
init(category: String, segment: [Double], uuid: String, videoDuration: Int) { init(category: String, segment: [Double], uuid: String, videoDuration: Int) {
self.category = category self.category = category
self.segment = segment self.segment = segment
@ -17,11 +29,11 @@ class Segment: ObservableObject, Hashable {
} }
func timeInSegment(_ time: CMTime) -> Bool { func timeInSegment(_ time: CMTime) -> Bool {
(segment.first! ... segment.last!).contains(time.seconds) (start ... end).contains(time.seconds)
} }
var skipTo: CMTime { var skipTo: CMTime {
CMTime(seconds: segment.last!, preferredTimescale: 1) CMTime(seconds: segment.last!, preferredTimescale: 1000)
} }
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {

View File

@ -29,7 +29,7 @@ final class SponsorBlockSegmentsProvider: ObservableObject {
private var parameters: [String: String] { private var parameters: [String: String] {
[ [
"videoID": id, "videoID": id,
"categories": JSON(categories).rawString(String.Encoding.utf8)! "categories": JSON(categories).rawString(String.Encoding.utf8)!,
] ]
} }
} }

View File

@ -85,6 +85,9 @@
37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
37C7A1D8267CACE10010EAD6 /* TrendingCategorySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17D267B4DDE00704544 /* TrendingCategorySelectionView.swift */; };
37C7A1D9267CACE60010EAD6 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17B267B4D9A00704544 /* VisualEffectView.swift */; };
37C7A1DA267CACF50010EAD6 /* TrendingCountrySelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountrySelectionView.swift */; };
37C7A9042679059200E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; }; 37C7A9042679059200E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
37C7A905267905AE00E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; }; 37C7A905267905AE00E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
37C7A906267905AF00E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; }; 37C7A906267905AF00E721B4 /* AVKeyValueStatus+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A9032679059200E721B4 /* AVKeyValueStatus+String.swift */; };
@ -655,9 +658,11 @@
37AAF29C26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */, 37AAF29C26741B5F007FC770 /* SubscriptionVideosProvider.swift in Sources */,
37141668267A83F9006CA35D /* StreamAVPlayerViewController.swift in Sources */, 37141668267A83F9006CA35D /* StreamAVPlayerViewController.swift in Sources */,
37EAD86B267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift in Sources */, 37EAD86B267B9C5600D9E01B /* SponsorBlockSegmentsProvider.swift in Sources */,
37C7A1DA267CACF50010EAD6 /* TrendingCountrySelectionView.swift in Sources */,
377FC7E6267A085600A6BBAF /* PlayerView.swift in Sources */, 377FC7E6267A085600A6BBAF /* PlayerView.swift in Sources */,
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */, 37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
37141677267A9AAD006CA35D /* TrendingState.swift in Sources */, 37141677267A9AAD006CA35D /* TrendingState.swift in Sources */,
37C7A1D9267CACE60010EAD6 /* VisualEffectView.swift in Sources */,
37D4B0E62671614900C925CA /* ContentView.swift in Sources */, 37D4B0E62671614900C925CA /* ContentView.swift in Sources */,
377FC7DC267A081800A6BBAF /* PopularVideosView.swift in Sources */, 377FC7DC267A081800A6BBAF /* PopularVideosView.swift in Sources */,
3714167F267AB55D006CA35D /* TrendingVideosProvider.swift in Sources */, 3714167F267AB55D006CA35D /* TrendingVideosProvider.swift in Sources */,
@ -682,6 +687,7 @@
37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */, 37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */,
3714167B267AA1CF006CA35D /* TrendingCountriesProvider.swift in Sources */, 3714167B267AA1CF006CA35D /* TrendingCountriesProvider.swift in Sources */,
377FC7DF267A082200A6BBAF /* VideosView.swift in Sources */, 377FC7DF267A082200A6BBAF /* VideosView.swift in Sources */,
37C7A1D8267CACE10010EAD6 /* TrendingCategorySelectionView.swift in Sources */,
37D4B19726717E1500C925CA /* Video.swift in Sources */, 37D4B19726717E1500C925CA /* Video.swift in Sources */,
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */, 37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */,
37CEE4B92677B63F005A1EFE /* StreamResolution.swift in Sources */, 37CEE4B92677B63F005A1EFE /* StreamResolution.swift in Sources */,