Opening videos by URL and local files

This commit is contained in:
Arkadiusz Fal
2022-11-10 18:11:28 +01:00
parent 34f7621f36
commit 402d1a2f79
40 changed files with 1158 additions and 126 deletions

View File

@@ -37,9 +37,24 @@ final class AVPlayerBackend: PlayerBackend {
avPlayer.timeControlStatus == .playing
}
var videoWidth: Double? {
if let width = avPlayer.currentItem?.presentationSize.width {
return Double(width)
}
return nil
}
var videoHeight: Double? {
if let height = avPlayer.currentItem?.presentationSize.height {
return Double(height)
}
return nil
}
var aspectRatio: Double {
#if os(iOS)
playerLayer.videoRect.width / playerLayer.videoRect.height
videoWidth! / videoHeight!
#else
VideoPlayerView.defaultAspectRatio
#endif
@@ -104,8 +119,17 @@ final class AVPlayerBackend: PlayerBackend {
preservingTime: Bool,
upgrading _: Bool
) {
if let url = stream.singleAssetURL {
if var url = stream.singleAssetURL {
model.logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")
if video.isLocal, video.localStreamIsFile, let localURL = video.localStream?.localURL {
guard localURL.startAccessingSecurityScopedResource() else {
model.navigation.presentAlert(title: "Could not open file")
return
}
url = localURL
}
loadSingleAsset(url, stream: stream, of: video, preservingTime: preservingTime)
} else {
model.logger.info("playing stream with many assets:")
@@ -317,6 +341,7 @@ final class AVPlayerBackend: PlayerBackend {
guard video == self.model.currentVideo else {
return
}
self.avPlayer.replaceCurrentItem(with: self.model.playerItem)
self.seekToPreservedTime { finished in
guard finished else {
@@ -373,7 +398,8 @@ final class AVPlayerBackend: PlayerBackend {
#if !os(macOS)
var externalMetadata = [
makeMetadataItem(.commonIdentifierTitle, value: video.title),
makeMetadataItem(.commonIdentifierTitle, value: video.displayTitle),
makeMetadataItem(.commonIdentifierArtist, value: video.displayAuthor),
makeMetadataItem(.quickTimeMetadataGenre, value: video.genre ?? ""),
makeMetadataItem(.commonIdentifierDescription, value: video.description ?? "")
]

View File

@@ -108,6 +108,10 @@ final class MPVBackend: PlayerBackend {
client?.outputFps ?? 0
}
var formattedOutputFps: String {
String(format: "%.2ffps", outputFps)
}
var hwDecoder: String {
client?.hwDecoder ?? "unknown"
}
@@ -120,6 +124,54 @@ final class MPVBackend: PlayerBackend {
client?.cacheDuration ?? 0
}
var videoFormat: String {
client?.videoFormat ?? "unknown"
}
var videoCodec: String {
client?.videoCodec ?? "unknown"
}
var currentVo: String {
client?.currentVo ?? "unknown"
}
var videoWidth: Double? {
if let width = client?.width, width != "unknown" {
return Double(width)
}
return nil
}
var videoHeight: Double? {
if let height = client?.height, height != "unknown" {
return Double(height)
}
return nil
}
var audioFormat: String {
client?.audioFormat ?? "unknown"
}
var audioCodec: String {
client?.audioCodec ?? "unknown"
}
var currentAo: String {
client?.currentAo ?? "unknown"
}
var audioChannels: String {
client?.audioChannels ?? "unknown"
}
var audioSampleRate: String {
client?.audioSampleRate ?? "unknown"
}
init() {
clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in
self?.getTimeUpdates()
@@ -230,6 +282,13 @@ final class MPVBackend: PlayerBackend {
startPlaying()
}
if video.isLocal, video.localStreamIsFile, let localStream = video.localStream {
guard localStream.localURL.startAccessingSecurityScopedResource() else {
self.model.navigation.presentAlert(title: "Could not open file")
return
}
}
self.client.loadFile(url, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in
self?.isLoadingVideo = true
}

View File

@@ -198,6 +198,50 @@ final class MPVClient: ObservableObject {
mpv.isNil ? 0.0 : getDouble("demuxer-cache-duration")
}
var videoFormat: String {
stringOrUnknown("video-format")
}
var videoCodec: String {
stringOrUnknown("video-codec")
}
var currentVo: String {
stringOrUnknown("current-vo")
}
var width: String {
stringOrUnknown("width")
}
var height: String {
stringOrUnknown("height")
}
var videoBitrate: Double {
mpv.isNil ? 0.0 : getDouble("video-bitrate")
}
var audioFormat: String {
stringOrUnknown("audio-params/format")
}
var audioCodec: String {
stringOrUnknown("audio-codec")
}
var currentAo: String {
stringOrUnknown("current-ao")
}
var audioChannels: String {
stringOrUnknown("audio-params/channels")
}
var audioSampleRate: String {
stringOrUnknown("audio-params/samplerate")
}
var aspectRatio: Double {
guard !mpv.isNil else { return VideoPlayerView.defaultAspectRatio }
let aspect = getDouble("video-params/aspect")
@@ -407,6 +451,10 @@ final class MPVClient: ObservableObject {
}
}
private func stringOrUnknown(_ name: String) -> String {
mpv.isNil ? "unknown" : (getString(name) ?? "unknown")
}
private var machine: String {
var systeminfo = utsname()
uname(&systeminfo)

View File

@@ -25,6 +25,9 @@ protocol PlayerBackend {
var aspectRatio: Double { get }
var controlsUpdates: Bool { get }
var videoWidth: Double? { get }
var videoHeight: Double? { get }
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream?
func canPlay(_ stream: Stream) -> Bool

View File

@@ -97,6 +97,10 @@ final class PlayerModel: ObservableObject {
@Published var currentItem: PlayerQueueItem! { didSet { handleCurrentItemChange() } }
@Published var videoBeingOpened: Video? { didSet { seek.reset() } }
@Published var historyVideos = [Video]()
@Published var queueItemBeingLoaded: PlayerQueueItem?
@Published var queueItemsToLoad = [PlayerQueueItem]()
@Published var historyItemBeingLoaded: Video.ID?
@Published var historyItemsToLoad = [Video.ID]()
@Published var preservedTime: CMTime?
@@ -373,7 +377,7 @@ final class PlayerModel: ObservableObject {
withBackend: PlayerBackend? = nil
) {
playerError = nil
if !upgrading {
if !upgrading, !video.isLocal {
resetSegments()
DispatchQueue.main.async { [weak self] in
@@ -440,7 +444,7 @@ final class PlayerModel: ObservableObject {
changeActiveBackend(from: activeBackend, to: backend)
}
guard let stream = streamByQualityProfile else {
guard let stream = ((availableStreams.count == 1 && availableStreams.first!.isLocal) ? availableStreams.first : nil) ?? streamByQualityProfile else {
return
}
@@ -842,8 +846,8 @@ final class PlayerModel: ObservableObject {
let currentTime = (backend.currentTime?.seconds.isFinite ?? false) ? backend.currentTime!.seconds : 0
var nowPlayingInfo: [String: AnyObject] = [
MPMediaItemPropertyTitle: video.title as AnyObject,
MPMediaItemPropertyArtist: video.author as AnyObject,
MPMediaItemPropertyTitle: video.displayTitle as AnyObject,
MPMediaItemPropertyArtist: video.displayAuthor as AnyObject,
MPNowPlayingInfoPropertyIsLiveStream: live as AnyObject,
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
@@ -952,4 +956,9 @@ final class PlayerModel: ObservableObject {
}
#endif
}
var formattedSize: String {
guard let videoWidth = backend?.videoWidth, let videoHeight = backend?.videoHeight else { return "unknown" }
return "\(String(format: "%.2f", videoWidth))\u{d7}\(String(format: "%.2f", videoHeight))"
}
}

View File

@@ -68,7 +68,7 @@ extension PlayerModel {
guard let playerInstance = self.playerInstance else { return }
let streamsInstance = video.streams.compactMap(\.instance).first
if video.streams.isEmpty || streamsInstance != playerInstance {
if !video.isLocal, video.streams.isEmpty || streamsInstance != playerInstance {
self.loadAvailableStreams(video)
} else {
self.availableStreams = self.streamsWithInstance(instance: playerInstance, streams: video.streams)
@@ -203,6 +203,7 @@ extension PlayerModel {
}
}
} else {
videoDetailsLoadHandler(video, item)
queue.insert(item, at: prepending ? 0 : queue.endIndex)
}
@@ -210,11 +211,22 @@ extension PlayerModel {
}
func prepareCurrentItemForHistory(finished: Bool = false) {
if !currentItem.isNil, Defaults[.saveHistory] {
if let video = currentVideo, !historyVideos.contains(where: { $0 == video }) {
historyVideos.append(video)
if let currentItem {
if Defaults[.saveHistory] {
if let video = currentVideo, !historyVideos.contains(where: { $0 == video }) {
historyVideos.append(video)
}
updateWatch(finished: finished)
}
if let video = currentItem.video,
video.isLocal,
video.localStreamIsFile,
let localURL = video.localStream?.localURL
{
logger.info("stopping security scoped resource access for \(localURL)")
localURL.stopAccessingSecurityScopedResource()
}
updateWatch(finished: finished)
}
}
@@ -253,9 +265,35 @@ extension PlayerModel {
func loadQueueVideoDetails(_ item: PlayerQueueItem) {
guard !accounts.current.isNil, !item.hasDetailsLoaded else { return }
playerAPI.loadDetails(item, completionHandler: { newItem in
if let index = self.queue.firstIndex(where: { $0.id == item.id }) {
self.queue[index] = newItem
let videoID = item.video?.videoID ?? item.videoID
if queueItemBeingLoaded == nil {
logger.info("loading queue details: \(videoID)")
queueItemBeingLoaded = item
} else {
logger.info("POSTPONING details load: \(videoID)")
queueItemsToLoad.append(item)
return
}
playerAPI.loadDetails(item, completionHandler: { [weak self] newItem in
guard let self else { return }
self.queue.filter { $0.videoID == item.videoID }.forEach { item in
if let index = self.queue.firstIndex(of: item) {
self.queue[index] = newItem
}
}
self.logger.info("LOADED queue details: \(videoID)")
if self.queueItemBeingLoaded == item {
self.logger.info("setting nothing loaded")
self.queueItemBeingLoaded = nil
}
if let item = self.queueItemsToLoad.popLast() {
self.loadQueueVideoDetails(item)
}
})
}

View File

@@ -42,7 +42,8 @@ struct PlayerQueueItem: Hashable, Identifiable, Defaults.Serializable {
}
var hasDetailsLoaded: Bool {
!video.isNil
guard let video else { return false }
return !video.streams.isEmpty
}
func hash(into hasher: inout Hasher) {

View File

@@ -25,7 +25,13 @@ struct PlayerQueueItemBridge: Defaults.Bridge {
}
}
var localURL = ""
if let video = value.video, video.isLocal {
localURL = video.localStream?.localURL.absoluteString ?? ""
}
return [
"localURL": localURL,
"videoID": value.videoID,
"playbackTime": playbackTime,
"videoDuration": videoDuration
@@ -33,12 +39,7 @@ struct PlayerQueueItemBridge: Defaults.Bridge {
}
func deserialize(_ object: Serializable?) -> Value? {
guard
let object,
let videoID = object["videoID"]
else {
return nil
}
guard let object else { return nil }
var playbackTime: CMTime?
var videoDuration: TimeInterval?
@@ -56,6 +57,19 @@ struct PlayerQueueItemBridge: Defaults.Bridge {
videoDuration = TimeInterval(duration)
}
if let localUrlString = object["localURL"],
!localUrlString.isEmpty,
let localURL = URL(string: localUrlString)
{
return PlayerQueueItem(
.local(localURL),
playbackTime: playbackTime,
videoDuration: videoDuration
)
}
guard let videoID = object["videoID"] else { return nil }
return PlayerQueueItem(
videoID: videoID,
playbackTime: playbackTime,